wp/wp-includes/js/heartbeat.js
changeset 0 d970ebf37754
child 5 5e2f62d02dcd
equal deleted inserted replaced
-1:000000000000 0:d970ebf37754
       
     1 /**
       
     2  * Heartbeat API
       
     3  *
       
     4  * Note: this API is "experimental" meaning it will likely change a lot
       
     5  * in the next few releases based on feedback from 3.6.0. If you intend
       
     6  * to use it, please follow the development closely.
       
     7  *
       
     8  * Heartbeat is a simple server polling API that sends XHR requests to
       
     9  * the server every 15 seconds and triggers events (or callbacks) upon
       
    10  * receiving data. Currently these 'ticks' handle transports for post locking,
       
    11  * login-expiration warnings, and related tasks while a user is logged in.
       
    12  *
       
    13  * Available filters in ajax-actions.php:
       
    14  * - heartbeat_received
       
    15  * - heartbeat_send
       
    16  * - heartbeat_tick
       
    17  * - heartbeat_nopriv_received
       
    18  * - heartbeat_nopriv_send
       
    19  * - heartbeat_nopriv_tick
       
    20  * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat()
       
    21  *
       
    22  * @since 3.6.0
       
    23  */
       
    24 
       
    25  // Ensure the global `wp` object exists.
       
    26 window.wp = window.wp || {};
       
    27 
       
    28 (function($){
       
    29 	var Heartbeat = function() {
       
    30 		var self = this,
       
    31 			running,
       
    32 			beat,
       
    33 			screenId = typeof pagenow != 'undefined' ? pagenow : '',
       
    34 			url = typeof ajaxurl != 'undefined' ? ajaxurl : '',
       
    35 			settings,
       
    36 			tick = 0,
       
    37 			queue = {},
       
    38 			interval,
       
    39 			connecting,
       
    40 			countdown = 0,
       
    41 			errorcount = 0,
       
    42 			tempInterval,
       
    43 			hasFocus = true,
       
    44 			isUserActive,
       
    45 			userActiveEvents,
       
    46 			winBlurTimeout,
       
    47 			frameBlurTimeout = -1,
       
    48 			hasConnectionError = null;
       
    49 
       
    50 		/**
       
    51 		 * Returns a boolean that's indicative of whether or not there is a connection error
       
    52 		 *
       
    53 		 * @returns boolean
       
    54 		 */
       
    55 		this.hasConnectionError = function() {
       
    56 			return !! hasConnectionError;
       
    57 		};
       
    58 
       
    59 		if ( typeof( window.heartbeatSettings ) == 'object' ) {
       
    60 			settings = $.extend( {}, window.heartbeatSettings );
       
    61 
       
    62 			// Add private vars
       
    63 			url = settings.ajaxurl || url;
       
    64 			delete settings.ajaxurl;
       
    65 			delete settings.nonce;
       
    66 
       
    67 			interval = settings.interval || 15; // default interval
       
    68 			delete settings.interval;
       
    69 			// The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec.
       
    70 			if ( interval < 15 )
       
    71 				interval = 15;
       
    72 			else if ( interval > 60 )
       
    73 				interval = 60;
       
    74 
       
    75 			interval = interval * 1000;
       
    76 
       
    77 			// 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set
       
    78 			screenId = screenId || settings.screenId || 'front';
       
    79 			delete settings.screenId;
       
    80 
       
    81 			// Add or overwrite public vars
       
    82 			$.extend( this, settings );
       
    83 		}
       
    84 
       
    85 		function time(s) {
       
    86 			if ( s )
       
    87 				return parseInt( (new Date()).getTime() / 1000 );
       
    88 
       
    89 			return (new Date()).getTime();
       
    90 		}
       
    91 
       
    92 		function isLocalFrame( frame ) {
       
    93 			var origin, src = frame.src;
       
    94 
       
    95 			if ( src && /^https?:\/\//.test( src ) ) {
       
    96 				origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host;
       
    97 
       
    98 				if ( src.indexOf( origin ) !== 0 )
       
    99 					return false;
       
   100 			}
       
   101 
       
   102 			try {
       
   103 				if ( frame.contentWindow.document )
       
   104 					return true;
       
   105 			} catch(e) {}
       
   106 
       
   107 			return false;
       
   108 		}
       
   109 
       
   110 		// Set error state and fire an event on XHR errors or timeout
       
   111 		function errorstate( error, status ) {
       
   112 			var trigger;
       
   113 
       
   114 			if ( error ) {
       
   115 				switch ( error ) {
       
   116 					case 'abort':
       
   117 						// do nothing
       
   118 						break;
       
   119 					case 'timeout':
       
   120 						// no response for 30 sec.
       
   121 						trigger = true;
       
   122 						break;
       
   123 					case 'parsererror':
       
   124 					case 'error':
       
   125 					case 'empty':
       
   126 					case 'unknown':
       
   127 						errorcount++;
       
   128 
       
   129 						if ( errorcount > 2 )
       
   130 							trigger = true;
       
   131 
       
   132 						break;
       
   133 				}
       
   134 
       
   135 				if ( 503 == status && false === hasConnectionError ) {
       
   136 					trigger = true;
       
   137 				}
       
   138 
       
   139 				if ( trigger && ! self.hasConnectionError() ) {
       
   140 					hasConnectionError = true;
       
   141 					$(document).trigger( 'heartbeat-connection-lost', [error, status] );
       
   142 				}
       
   143 			} else if ( self.hasConnectionError() ) {
       
   144 				errorcount = 0;
       
   145 				hasConnectionError = false;
       
   146 				$(document).trigger( 'heartbeat-connection-restored' );
       
   147 			} else if ( null === hasConnectionError ) {
       
   148 				hasConnectionError = false;
       
   149 			}
       
   150 		}
       
   151 
       
   152 		function connect() {
       
   153 			var send = {}, data, i, empty = true,
       
   154 			nonce = typeof window.heartbeatSettings == 'object' ? window.heartbeatSettings.nonce : '';
       
   155 			tick = time();
       
   156 
       
   157 			data = $.extend( {}, queue );
       
   158 			// Clear the data queue, anything added after this point will be send on the next tick
       
   159 			queue = {};
       
   160 
       
   161 			$(document).trigger( 'heartbeat-send', [data] );
       
   162 
       
   163 			for ( i in data ) {
       
   164 				if ( data.hasOwnProperty( i ) ) {
       
   165 					empty = false;
       
   166 					break;
       
   167 				}
       
   168 			}
       
   169 
       
   170 			// If nothing to send (nothing is expecting a response),
       
   171 			// schedule the next tick and bail
       
   172 			if ( empty && ! self.hasConnectionError() ) {
       
   173 				connecting = false;
       
   174 				next();
       
   175 				return;
       
   176 			}
       
   177 
       
   178 			send.data = data;
       
   179 			send.interval = interval / 1000;
       
   180 			send._nonce = nonce;
       
   181 			send.action = 'heartbeat';
       
   182 			send.screen_id = screenId;
       
   183 			send.has_focus = hasFocus;
       
   184 
       
   185 			connecting = true;
       
   186 			self.xhr = $.ajax({
       
   187 				url: url,
       
   188 				type: 'post',
       
   189 				timeout: 30000, // throw an error if not completed after 30 sec.
       
   190 				data: send,
       
   191 				dataType: 'json'
       
   192 			}).done( function( response, textStatus, jqXHR ) {
       
   193 				var new_interval;
       
   194 
       
   195 				if ( ! response )
       
   196 					return errorstate( 'empty' );
       
   197 
       
   198 				// Clear error state
       
   199 				if ( self.hasConnectionError() )
       
   200 					errorstate();
       
   201 
       
   202 				if ( response.nonces_expired ) {
       
   203 					$(document).trigger( 'heartbeat-nonces-expired' );
       
   204 					return;
       
   205 				}
       
   206 
       
   207 				// Change the interval from PHP
       
   208 				if ( response.heartbeat_interval ) {
       
   209 					new_interval = response.heartbeat_interval;
       
   210 					delete response.heartbeat_interval;
       
   211 				}
       
   212 
       
   213 				self.tick( response, textStatus, jqXHR );
       
   214 
       
   215 				// do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast'
       
   216 				if ( new_interval )
       
   217 					self.interval.call( self, new_interval );
       
   218 			}).always( function() {
       
   219 				connecting = false;
       
   220 				next();
       
   221 			}).fail( function( jqXHR, textStatus, error ) {
       
   222 				errorstate( textStatus || 'unknown', jqXHR.status );
       
   223 				self.error( jqXHR, textStatus, error );
       
   224 			});
       
   225 		}
       
   226 
       
   227 		function next() {
       
   228 			var delta = time() - tick, t = interval;
       
   229 
       
   230 			if ( ! running )
       
   231 				return;
       
   232 
       
   233 			if ( ! hasFocus ) {
       
   234 				t = 100000; // 100 sec. Post locks expire after 120 sec.
       
   235 			} else if ( countdown > 0 && tempInterval ) {
       
   236 				t = tempInterval;
       
   237 				countdown--;
       
   238 			}
       
   239 
       
   240 			window.clearTimeout(beat);
       
   241 
       
   242 			if ( delta < t ) {
       
   243 				beat = window.setTimeout(
       
   244 					function(){
       
   245 						if ( running )
       
   246 							connect();
       
   247 					},
       
   248 					t - delta
       
   249 				);
       
   250 			} else {
       
   251 				connect();
       
   252 			}
       
   253 		}
       
   254 
       
   255 		function blurred() {
       
   256 			window.clearTimeout(winBlurTimeout);
       
   257 			window.clearTimeout(frameBlurTimeout);
       
   258 			winBlurTimeout = frameBlurTimeout = 0;
       
   259 
       
   260 			hasFocus = false;
       
   261 		}
       
   262 
       
   263 		function focused() {
       
   264 			window.clearTimeout(winBlurTimeout);
       
   265 			window.clearTimeout(frameBlurTimeout);
       
   266 			winBlurTimeout = frameBlurTimeout = 0;
       
   267 
       
   268 			isUserActive = time();
       
   269 
       
   270 			if ( hasFocus )
       
   271 				return;
       
   272 
       
   273 			hasFocus = true;
       
   274 			window.clearTimeout(beat);
       
   275 
       
   276 			if ( ! connecting )
       
   277 				next();
       
   278 		}
       
   279 
       
   280 		function setFrameEvents() {
       
   281 			$('iframe').each( function( i, frame ){
       
   282 				if ( ! isLocalFrame( frame ) )
       
   283 					return;
       
   284 
       
   285 				if ( $.data( frame, 'wp-heartbeat-focus' ) )
       
   286 					return;
       
   287 
       
   288 				$.data( frame, 'wp-heartbeat-focus', 1 );
       
   289 
       
   290 				$( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function(e) {
       
   291 					focused();
       
   292 				}).on('blur.wp-heartbeat-focus', function(e) {
       
   293 					setFrameEvents();
       
   294 					frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 );
       
   295 				});
       
   296 			});
       
   297 		}
       
   298 
       
   299 		$(window).on( 'blur.wp-heartbeat-focus', function(e) {
       
   300 			setFrameEvents();
       
   301 			winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 );
       
   302 		}).on( 'focus.wp-heartbeat-focus', function() {
       
   303 			$('iframe').each( function( i, frame ) {
       
   304 				if ( !isLocalFrame( frame ) )
       
   305 					return;
       
   306 
       
   307 				$.removeData( frame, 'wp-heartbeat-focus' );
       
   308 				$( frame.contentWindow ).off( '.wp-heartbeat-focus' );
       
   309 			});
       
   310 
       
   311 			focused();
       
   312 		});
       
   313 
       
   314 		function userIsActive() {
       
   315 			userActiveEvents = false;
       
   316 			$(document).off( '.wp-heartbeat-active' );
       
   317 			$('iframe').each( function( i, frame ) {
       
   318 				if ( ! isLocalFrame( frame ) )
       
   319 					return;
       
   320 
       
   321 				$( frame.contentWindow ).off( '.wp-heartbeat-active' );
       
   322 			});
       
   323 
       
   324 			focused();
       
   325 		}
       
   326 
       
   327 		// Set 'hasFocus = true' if user is active and the window is in the background.
       
   328 		// Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus.
       
   329 		function checkUserActive() {
       
   330 			var lastActive = isUserActive ? time() - isUserActive : 0;
       
   331 
       
   332 			// Throttle down when no mouse or keyboard activity for 5 min
       
   333 			if ( lastActive > 300000 && hasFocus )
       
   334 				 blurred();
       
   335 
       
   336 			if ( ! userActiveEvents ) {
       
   337 				$(document).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } );
       
   338 
       
   339 				$('iframe').each( function( i, frame ) {
       
   340 					if ( ! isLocalFrame( frame ) )
       
   341 						return;
       
   342 
       
   343 					$( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } );
       
   344 				});
       
   345 
       
   346 				userActiveEvents = true;
       
   347 			}
       
   348 		}
       
   349 
       
   350 		// Check for user activity every 30 seconds.
       
   351 		window.setInterval( function(){ checkUserActive(); }, 30000 );
       
   352 		$(document).ready( function() {
       
   353 			// Start one tick (15 sec) after DOM ready
       
   354 			running = true;
       
   355 			tick = time();
       
   356 			next();
       
   357 		});
       
   358 
       
   359 		this.hasFocus = function() {
       
   360 			return hasFocus;
       
   361 		};
       
   362 
       
   363 		/**
       
   364 		 * Get/Set the interval
       
   365 		 *
       
   366 		 * When setting to 'fast', the interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec).
       
   367 		 * If the window doesn't have focus, the interval slows down to 2 min.
       
   368 		 *
       
   369 		 * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec)
       
   370 		 * @param string ticks Used with speed = 'fast', how many ticks before the speed reverts back
       
   371 		 * @return int Current interval in seconds
       
   372 		 */
       
   373 		this.interval = function( speed, ticks ) {
       
   374 			var reset, seconds;
       
   375 			ticks = parseInt( ticks, 10 ) || 30;
       
   376 			ticks = ticks < 1 || ticks > 30 ? 30 : ticks;
       
   377 
       
   378 			if ( speed ) {
       
   379 				switch ( speed ) {
       
   380 					case 'fast':
       
   381 						seconds = 5;
       
   382 						countdown = ticks;
       
   383 						break;
       
   384 					case 'slow':
       
   385 						seconds = 60;
       
   386 						countdown = 0;
       
   387 						break;
       
   388 					case 'long-polling':
       
   389 						// Allow long polling, (experimental)
       
   390 						interval = 0;
       
   391 						return 0;
       
   392 						break;
       
   393 					default:
       
   394 						seconds = 15;
       
   395 						countdown = 0;
       
   396 				}
       
   397 
       
   398 				// Reset when the new interval value is lower than the current one
       
   399 				reset = seconds * 1000 < interval;
       
   400 
       
   401 				if ( countdown > 0 ) {
       
   402 					tempInterval = seconds * 1000;
       
   403 				} else {
       
   404 					interval = seconds * 1000;
       
   405 					tempInterval = 0;
       
   406 				}
       
   407 
       
   408 				if ( reset )
       
   409 					next();
       
   410 			}
       
   411 
       
   412 			if ( ! hasFocus )
       
   413 				return 120;
       
   414 
       
   415 			return tempInterval ? tempInterval / 1000 : interval / 1000;
       
   416 		};
       
   417 
       
   418 		/**
       
   419 		 * Enqueue data to send with the next XHR
       
   420 		 *
       
   421 		 * As the data is sent later, this function doesn't return the XHR response.
       
   422 		 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example:
       
   423 		 *		$(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) {
       
   424 		 *			// code
       
   425 		 *		});
       
   426 		 * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'.
       
   427 		 * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle.
       
   428 		 *
       
   429 		 * $param string handle Unique handle for the data. The handle is used in PHP to receive the data.
       
   430 		 * $param mixed data The data to send.
       
   431 		 * $param bool dont_overwrite Whether to overwrite existing data in the queue.
       
   432 		 * $return bool Whether the data was queued or not.
       
   433 		 */
       
   434 		this.enqueue = function( handle, data, dont_overwrite ) {
       
   435 			if ( handle ) {
       
   436 				if ( dont_overwrite && this.isQueued( handle ) )
       
   437 					return false;
       
   438 
       
   439 				queue[handle] = data;
       
   440 				return true;
       
   441 			}
       
   442 			return false;
       
   443 		};
       
   444 
       
   445 		/**
       
   446 		 * Check if data with a particular handle is queued
       
   447 		 *
       
   448 		 * $param string handle The handle for the data
       
   449 		 * $return bool Whether some data is queued with this handle
       
   450 		 */
       
   451 		this.isQueued = function( handle ) {
       
   452 			if ( handle )
       
   453 				return queue.hasOwnProperty( handle );
       
   454 		};
       
   455 
       
   456 		/**
       
   457 		 * Remove data with a particular handle from the queue
       
   458 		 *
       
   459 		 * $param string handle The handle for the data
       
   460 		 * $return void
       
   461 		 */
       
   462 		this.dequeue = function( handle ) {
       
   463 			if ( handle )
       
   464 				delete queue[handle];
       
   465 		};
       
   466 
       
   467 		/**
       
   468 		 * Get data that was enqueued with a particular handle
       
   469 		 *
       
   470 		 * $param string handle The handle for the data
       
   471 		 * $return mixed The data or undefined
       
   472 		 */
       
   473 		this.getQueuedItem = function( handle ) {
       
   474 			if ( handle )
       
   475 				return this.isQueued( handle ) ? queue[handle] : undefined;
       
   476 		};
       
   477 	};
       
   478 
       
   479 	$.extend( Heartbeat.prototype, {
       
   480 		tick: function( data, textStatus, jqXHR ) {
       
   481 			$(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] );
       
   482 		},
       
   483 		error: function( jqXHR, textStatus, error ) {
       
   484 			$(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] );
       
   485 		}
       
   486 	});
       
   487 
       
   488 	wp.heartbeat = new Heartbeat();
       
   489 
       
   490 }(jQuery));