Index: wp-includes/js/heartbeat.js
===================================================================
--- wp-includes/js/heartbeat.js	(revision 24720)
+++ wp-includes/js/heartbeat.js	(working copy)
@@ -1,474 +1,893 @@
-/**
- * Heartbeat API
- *
- * Heartbeat is a simple server polling API that sends XHR requests to
- * the server every 15 seconds and triggers events (or callbacks) upon
- * receiving data. Currently these 'ticks' handle transports for post locking,
- * login-expiration warnings, and related tasks while a user is logged in.
- *
- * Available filters in ajax-actions.php:
- * - heartbeat_received
- * - heartbeat_send
- * - heartbeat_tick
- * - heartbeat_nopriv_received
- * - heartbeat_nopriv_send
- * - heartbeat_nopriv_tick
- * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat()
- *
- * @since 3.6.0
- */
+( function( window, $, undefined ) {
 
- // Ensure the global `wp` object exists.
-window.wp = window.wp || {};
+	// pull the correct document into scope
+	var document = window.document;
 
-(function($){
-	var Heartbeat = function() {
-		var self = this,
-			running,
-			beat,
-			screenId = typeof pagenow != 'undefined' ? pagenow : '',
-			url = typeof ajaxurl != 'undefined' ? ajaxurl : '',
-			settings,
-			tick = 0,
-			queue = {},
-			interval,
-			connecting,
-			countdown = 0,
-			errorcount = 0,
-			tempInterval,
-			hasFocus = true,
-			isUserActive,
-			userActiveEvents,
-			winBlurTimeout,
-			frameBlurTimeout = -1;
+	/**
+	 * Heartbeat API
+	 *
+	 * Heartbeat is a simple server polling API that sends XHR requests to the server every 15 seconds and triggers events
+	 * (or callbacks) upon receiving data. Currently these 'ticks' handle transports for post locking, login-expiration
+	 * warnings, and related tasks while a user is logged in.
+	 *
+	 * todo: if this code is run multiple times in different iframes, there could be a problem with iframes becoming
+	 * inactive but activity occurring in the main window which is really weird... To fix this, we need to check if the
+	 * current window is the top window just like ads do. This could either be a major bug OR something that's intended
+	 * but either way should be confirmed ASAP.
+	 *
+	 * Available filters in ajax-actions.php:
+	 * 		heartbeat_received
+	 * 		heartbeat_send
+	 * 		heartbeat_tick
+	 * 		heartbeat_nopriv_received
+	 * 		heartbeat_nopriv_send
+	 * 		heartbeat_nopriv_tick
+	 * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat()
+	 *
+	 * @since 3.6.0
+	 * @type {*|{}}
+	 */
+	function Heartbeat() {
 
-		this.autostart = true;
-		this.connectionLost = false;
+		/**
+		 * Container for all of the cached objects that we'll need references to later
+		 *
+		 * @type {{$document: null, $window: null, windowBlurTimer: null, frameBlurTimer: null, heartbeatTimer: null, ajaxRequest: null}}
+		 * @private
+		 */
+		var _Cache = {
+			$document : null,
+			$window : null,
+			windowBlurTimer : null,
+			frameBlurTimer : null,
+			heartbeatTimer : null,
+			ajaxRequest : null
+		};
 
-		if ( typeof( window.heartbeatSettings ) == 'object' ) {
-			settings = $.extend( {}, window.heartbeatSettings );
 
-			// Add private vars
-			url = settings.ajaxurl || url;
-			delete settings.ajaxurl;
-			delete settings.nonce;
+		/**
+		 * Container for all of the settings that this module needs to track
+		 *
+		 * @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}}
+		 * @private
+		 */
+		var _Settings = {
+			shouldAutoStart : true,
+			hasActiveConnection : false,
+			errorCountSinceConnected : 0,
+			ajaxURL : '',
+			nonce : '',
+			heartbeatInterval : 15,
+			temporaryHeartbeatInterval : 0,
+			screenId : '',
+			hasFocus : false,
+			userIsActive : false,
+			lastUserActivityTimestamp : _getUnixTimestamp(),
+			isConnecting : false,
+			isRunning : false,
+			userActivityCheckInterval : 30000,
+			lastHeartbeatTick : 0,
+			countDown : 0,
+			ajaxRequestTimeout : 30000
+		};
 
-			interval = settings.interval || 15; // default interval
-			delete settings.interval;
-			// The interval can be from 15 to 60 sec. and can be set temporarily to 5 sec.
-			if ( interval < 15 )
-				interval = 15;
-			else if ( interval > 60 )
-				interval = 60;
+		/**
+		 * Container for any AJAX data that will sent through the next heartbeat pulse
+		 *
+		 * @type {{}}
+		 * @private
+		 */
+		var _QueuedAjaxData = {};
 
-			interval = interval * 1000;
+		/**
+		 * Handles setting up this object so that things are properly setup before it's used anywhere
+		 *
+		 * @private
+		 */
+		function _initialize() {
+			_buildSettings();
+			_cacheObjects();
+			_startCheckingIfTheUserIsActive();
+			_bindWindowEvents();
 
-			// 'screenId' can be added from settings on the front-end where the JS global 'pagenow' is not set
-			screenId = screenId || settings.screenId || 'front';
-			delete settings.screenId;
+			// check to see if we need to automatically start this module
+			if ( _Settings.shouldAutoStart === true ) {
+				_Cache.$document.ready( _startAutomaticTick );
+			}
+		}
 
-			// Add or overwrite public vars
-			$.extend( this, settings );
+		/**
+		 * Starts the automatic tick if the application should automatically start. Should only be called from `_initialize()`
+		 *
+		 * @private
+		 */
+		function _startAutomaticTick() {
+			_Settings.isRunning = true;
+			_Settings.lastHeartbeatTick = _getUnixTimestamp();
+			_nextTick();
 		}
 
-		function time(s) {
-			if ( s )
-				return parseInt( (new Date()).getTime() / 1000 );
-
-			return (new Date()).getTime();
+		/**
+		 * Starts the interval timer for checking if the user is currently active or not
+		 *
+		 * @private
+		 */
+		function _startCheckingIfTheUserIsActive() {
+			setInterval( _checkIfUserIsStillActive, _Settings.userActivityCheckInterval );
 		}
 
-		function isLocalFrame( frame ) {
-			var origin, src = frame.src;
+		/**
+		 * Handles standardizing our internal settings based on settings that might have been localized from the server.
+		 * We didn't extend the global window `heartbeatSettings` object because we don't want to make use of `delete`
+		 * and have to manage everything we don't care about.
+		 *
+		 * @private
+		 */
+		function _buildSettings() {
+			if ( typeof window.heartbeatSettings !== 'object' ) {
+				return;
+			}
 
-			if ( src && /^https?:\/\//.test( src ) ) {
-				origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host;
+			// temporarily cache the window heartbeatSettings for easier access
+			var tempSettings = window.heartbeatSettings;
 
-				if ( src.indexOf( origin ) !== 0 )
-					return false;
+			// setup the ajax URL
+			_Settings.ajaxURL = window.ajaxurl || '';
+			_Settings.ajaxURL = tempSettings.ajaxurl || _Settings.ajaxURL;
+
+			// setup the nonce
+			_Settings.nonce = tempSettings.nonce || '';
+
+			// setup the heartbeat interval - force it to an integer. If the value of tempSettings.interval is incorrect
+			// and cannot be parsed, we still default to 10
+			_Settings.heartbeatInterval = parseInt( tempSettings.interval, 10 ) || _Settings.heartbeatInterval;
+
+			// keep the heartbeat interval within bounds
+			if ( _Settings.heartbeatInterval < 15 ) {
+				_Settings.heartbeatInterval = 15;
+			} else if( _Settings.heartbeatInterval > 60 ) {
+				_Settings.heartbeatInterval = 60;
 			}
 
-			try {
-				if ( frame.contentWindow.document )
-					return true;
-			} catch(e) {}
+			// make sure the interval is in milliseconds now
+			_Settings.heartbeatInterval *= 1000;
 
-			return false;
+			// setup the screenId now
+			// screenId can be added from settings on the front-end where the JS global `pagenow` is not set
+			_Settings.screenId = window.pagenow || ( settings.screenId || '' );
 		}
 
-		// Set error state and fire an event on XHR errors or timeout
-		function errorstate( error ) {
-			var trigger;
+		/**
+		 * Caches any objects we might be using during the lifetime of this module
+		 *
+		 * @private
+		 */
+		function _cacheObjects() {
+			_Cache.$document = $( document );
+			_Cache.$window = $( window );
+		}
 
-			if ( error ) {
-				switch ( error ) {
-					case 'abort':
-						// do nothing
-						break;
-					case 'timeout':
-						// no response for 30 sec.
-						trigger = true;
-						break;
-					case 'parsererror':
-					case 'error':
-					case 'empty':
-					case 'unknown':
-						errorcount++;
+		/**
+		 * Gets the current unix timestamp
+		 *
+		 * @returns {number}
+		 */
+		function _getUnixTimestamp() {
+			return ( new Date() ).getTime();
+		}
 
-						if ( errorcount > 2 )
-							trigger = true;
+		/**
+		 * Handles enqueuing data to be sent along with the next heartbeat pulse. We require a key to be set. Note that
+		 * a value is not required in the event that a user might want to dequeue a key from being passed along.
+		 *
+		 * As the data is sent later, this function doesn't return the XHR response.
+		 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example:
+		 *		$(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) {
+		 *			// code
+		 *		});
+		 * If the same `key` is used more than once, the data is not overwritten when the third argument is `true`.
+		 * Use wp.heartbeat.isQueued( 'key' ) to see if any data is already queued for that key.
+		 *
+		 * @param key Unique string for the data. The handle is used in PHP to receive the data.
+		 * @param value The data to send.
+		 * @param overwriteExistingData Whether to overwrite existing data in the queue.
+		 * @returns {boolean} Whether the data was queued or not.
+		 * @private
+		 */
+		function _enqueueData( key, value, overwriteExistingData ) {
+			overwriteExistingData = ( typeof overwriteExistingData === 'boolean' ) ? overwriteExistingData : false;
 
-						break;
+			if ( key !== undefined ) {
+				if ( _QueuedAjaxData.hasOwnProperty( key ) === true && overwriteExistingData === false ) {
+					return false;
 				}
 
-				if ( trigger && ! self.connectionLost ) {
-					self.connectionLost = true;
-					$(document).trigger( 'heartbeat-connection-lost', [error] );
-				}
-			} else if ( self.connectionLost ) {
-				errorcount = 0;
-				self.connectionLost = false;
-				$(document).trigger( 'heartbeat-connection-restored' );
+				_QueuedAjaxData[ key ] = value;
+				return true;
 			}
+			return false;
 		}
 
-		function connect() {
-			var send = {}, data, i, empty = true,
-			nonce = typeof window.heartbeatSettings == 'object' ? window.heartbeatSettings.nonce : '';
-			tick = time();
+		/**
+		 * Check if data with a particular handle is queued.
+		 *
+		 * @param key The handle for the data
+		 * @returns {*} The data queued with that handle or null
+		 * @private
+		 */
+		function _dataIsQueued( key ) {
+			return _QueuedAjaxData[ key ];
+		}
 
-			data = $.extend( {}, queue );
-			// Clear the data queue, anything added after this point will be send on the next tick
-			queue = {};
+		/**
+		 * If a value is specified, this function will set the internal value of the `shouldAutoStart` setting. Always
+		 * returns the value of `shouldAutoStart`
+		 *
+		 * @param value
+		 * @returns boolean
+		 * @private
+		 */
+		function _shouldAutoStart( value ) {
+			if ( typeof value === 'boolean' ) {
+				return _Settings.shouldAutoStart = value;
+			}
 
-			$(document).trigger( 'heartbeat-send', [data] );
+			return _Settings.shouldAutoStart;
+		}
 
-			for ( i in data ) {
-				if ( data.hasOwnProperty( i ) ) {
-					empty = false;
-					break;
-				}
+		/**
+		 * Determines if the iframe passed to this function is indeed an iframe and whether or not the src of that iframe
+		 * is local to us. The iframe will be classified as `local` if and only if it has either:
+		 * 	1. the same domain name and protocol as the currently open window
+		 * 	2. the document object can be accessed on the frame which means that our browser recognizes the iframe src
+		 * 		as one of our own.
+		 *
+		 * @param iframe
+		 * @returns {boolean}
+		 * @private
+		 */
+		function _isLocalFrame( iframe ) {
+			if ( iframe.nodeName !== 'IFRAME' ) {
+				return false;
 			}
 
-			// If nothing to send (nothing is expecting a response),
-			// schedule the next tick and bail
-			if ( empty && ! self.connectionLost ) {
-				connecting = false;
-				next();
-				return;
+			var origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host;
+			var src = iframe.getAttribute( 'src' );
+
+			if ( /^https?:\/\//.test( src ) === true && src.indexOf( origin ) !== 0 ) {
+				return false;
 			}
 
-			send.data = data;
-			send.interval = interval / 1000;
-			send._nonce = nonce;
-			send.action = 'heartbeat';
-			send.screen_id = screenId;
-			send.has_focus = hasFocus;
+			if ( iframe.contentWindow !== undefined && iframe.contentWindow.document !== undefined ) {
+				return true;
+			}
 
-			connecting = true;
-			self.xhr = $.ajax({
-				url: url,
-				type: 'post',
-				timeout: 30000, // throw an error if not completed after 30 sec.
-				data: send,
-				dataType: 'json'
-			}).done( function( response, textStatus, jqXHR ) {
-				var new_interval;
+			return false;
+		}
 
-				if ( ! response )
-					return errorstate( 'empty' );
+		/**
+		 * If a value is specified, this function will set the internal value of the `hasActiveConnection` setting.
+		 * Always returns the value of `hasActiveConnection`
+		 *
+		 * @param value (optional)
+		 * @returns boolean
+		 * @private
+		 */
+		function _hasActiveConnection( value ) {
+			if ( typeof value === 'boolean' ) {
+				_Settings.hasActiveConnection = value;
+				return value;
+			}
 
-				// Clear error state
-				if ( self.connectionLost )
-					errorstate();
+			return _Settings.hasActiveConnection;
+		}
 
-				if ( response.nonces_expired ) {
-					$(document).trigger( 'heartbeat-nonces-expired' );
-					return;
-				}
+		/**
+		 * Checks the error passed to this function and takes the appropriate action necessary. This function mainly
+		 * catches the transitions from an active connection to that of a non-active connection.
+		 *
+		 * @param error
+		 * @private
+		 */
+		function _triggerError( error ) {
+			var trigger = false;
+			error = error || false;
 
-				// Change the interval from PHP
-				if ( response.heartbeat_interval ) {
-					new_interval = response.heartbeat_interval;
-					delete response.heartbeat_interval;
+			if ( error === 'abort' ) {
+				return;
+			} else if ( error === 'timeout' ) {
+				trigger = true;
+			} else if ( error === 'unknown' || error === 'parsererror' || error === 'error' || error === 'empty' ) {
+				_Settings.errorCountSinceConnected++;
+				if ( _Settings.errorCountSinceConnected > 2 ) {
+					trigger = true;
 				}
+			}
 
-				self.tick( response, textStatus, jqXHR );
+			// take action if we need to trigger things
+			if ( trigger === true && _hasActiveConnection() === true ) {
+				_hasActiveConnection( false );
+				_Cache.$document.trigger( 'heartbeat-connection-lost', [ error ] );
+			} else if ( _hasActiveConnection() === false ) {
+				_Settings.errorCountSinceConnected = 0;
+				_hasActiveConnection( true );
+				_Cache.$document.trigger( 'heartbeat-connection-restored' );
+			}
+		}
 
-				// do this last, can trigger the next XHR if connection time > 5 sec. and new_interval == 'fast'
-				if ( new_interval )
-					self.interval.call( self, new_interval );
-			}).always( function() {
-				connecting = false;
-				next();
-			}).fail( function( jqXHR, textStatus, error ) {
-				errorstate( textStatus || 'unknown' );
-				self.error( jqXHR, textStatus, error );
-			});
-		};
+		/**
+		 * Handles connecting to the server-side heartbeat API and sending any queued data along.
+		 *
+		 * @private
+		 */
+		function _connect() {
+			var dataToSend = $.extend( {}, _QueuedAjaxData );
+			_clearAjaxDataQueue();
 
-		function next() {
-			var delta = time() - tick, t = interval;
+			// let other applications know that we're sending data
+			_Cache.$document.trigger( 'heartbeat-send', [ dataToSend ] );
 
-			if ( ! running )
+			// make sure there is data to send and that we have an active connection. If this criteria is met, bail from
+			// this function
+			if ( _objectIsEmpty( dataToSend ) === true && _hasActiveConnection() === true ) {
+				_Settings.isConnecting = false;
+				_nextTick();
 				return;
+			}
 
-			if ( ! hasFocus ) {
-				t = 120000; // 2 min
-			} else if ( countdown > 0 && tempInterval ) {
-				t = tempInterval;
-				countdown--;
+			var ajaxData = _buildAjaxData( dataToSend );
+
+			// keep track of when we're connecting for this ajax call
+			_Settings.isConnecting = true;
+
+			// increment the timer tick
+			_Settings.lastHeartbeatTick = _getUnixTimestamp();
+
+			// initiate a new ajax request
+			_Cache.ajaxRequest = $.ajax( {
+				url : _Settings.ajaxURL,
+				type : 'post',
+				timeout : _Settings.ajaxRequestTimeout,
+				data : ajaxData,
+				dataType : 'json'
+			} );
+
+			// setup our promises
+			_Cache.ajaxRequest.done( _onAjaxDone );
+			_Cache.ajaxRequest.always( _onAjaxAlwaysPromise );
+			_Cache.ajaxRequest.fail( _onAjaxFailed );
+		}
+
+		/**
+		 * Handles taking action when the heartbeat pulse AJAX call is finished, whether fail or succeed
+		 *
+		 * @param response
+		 * @param textStatus
+		 * @param jqXHR
+		 * @private
+		 */
+		function _onAjaxDone( response, textStatus, jqXHR  ) {
+			var cachedHeartbeatInterval = null;
+
+			// make sure we got a response
+			if ( ! response ) {
+				_triggerError( 'empty' );
+				return;
 			}
 
-			window.clearTimeout(beat);
+			// clear the error state if the connection was lost already
+			if ( _hasActiveConnection() === false ) {
+				// todo: this was ported from old code but I think it's confusing that we are firing triggerError to clear something
+				_triggerError();
+			}
 
-			if ( delta < t ) {
-				beat = window.setTimeout(
-					function(){
-						if ( running )
-							connect();
-					},
-					t - delta
-				);
-			} else {
-				connect();
+			// check to see if the nonce has expired
+			// todo: this could use a strict check
+			if ( response.nonces_expired ) {
+				_Cache.$document.trigger( 'heartbeat-nonces-expired' );
 			}
+
+			// check to see if a new interval needs to be sent
+			// todo: this could use a strict check
+			if ( response.heartbeat_interval ) {
+				cachedHeartbeatInterval = response.heartbeat_interval;
+
+				// todo: what's the purpose of deleting this?
+				delete response.heartbeat_interval;
+			}
+
+			// notify other applications of this heartbeat tick
+			_Cache.$document.trigger( 'heartbeat-tick', [ response, textStatus, jqXHR ] );
+
+			// check to see if we set a value, do this last, can trigger the next XHR if connection time > 5 sec. and
+			// new_interval == 'fast'
+			if ( cachedHeartbeatInterval !== null ) {
+				_interval( cachedHeartbeatInterval );
+			}
 		}
 
-		function blurred() {
-			window.clearTimeout(winBlurTimeout);
-			window.clearTimeout(frameBlurTimeout);
-			winBlurTimeout = frameBlurTimeout = 0;
+		/**
+		 * Handles taking action when the heartbeat pulse is completed and has failed
+		 *
+		 * @param jqXHR
+		 * @param textStatus
+		 * @param error
+		 * @private
+		 */
+		function _onAjaxFailed( jqXHR, textStatus, error ) {
+			_triggerError( textStatus || 'unknown' );
+			_Cache.$document.trigger( 'heartbeat-error', [ jqXHR, textStatus, error ] );
+		}
 
-			hasFocus = false;
+		/**
+		 * Handles any actions to take for the always part of the Ajax promise
+		 *
+		 * @private
+		 */
+		function _onAjaxAlwaysPromise() {
+			_Settings.isConnecting = false;
+			_nextTick();
 		}
 
-		function focused() {
-			window.clearTimeout(winBlurTimeout);
-			window.clearTimeout(frameBlurTimeout);
-			winBlurTimeout = frameBlurTimeout = 0;
+		/**
+		 * Builds the data object literal that we'll use for initiating an AJAX request for each heartbeat pulse
+		 *
+		 * @param queuedData
+		 * @returns {{}}
+		 * @private
+		 */
+		function _buildAjaxData( queuedData ) {
+			return {
+				data : queuedData,
+				interval : ( _Settings.heartbeatInterval / 1000 ),
+				_nonce : _Settings.nonce,
+				action : 'heartbeat',
+				screen_id : _Settings.screenId,
+				has_focus : _Settings.hasFocus
+			};
+		}
 
-			isUserActive = time();
+		/**
+		 * Checks to see if the object literal passed to this function is currently empty or not.
+		 *
+		 * @param object
+		 * @returns {boolean}
+		 * @private
+		 */
+		function _objectIsEmpty( object ) {
+			for ( var key in object ) {
+				if ( object.hasOwnProperty( key ) ) {
+					return false;
+				}
+			}
 
-			if ( hasFocus )
-				return;
+			return true;
+		}
 
-			hasFocus = true;
-			window.clearTimeout(beat);
+		/**
+		 * Handles clearing the current AJAX data queue. This was moved to a function for clarity moving forward along
+		 * with the idea that we could possibly trigger events here or do additional things.
+		 *
+		 * @private
+		 */
+		function _clearAjaxDataQueue() {
+			_QueuedAjaxData = {};
+		}
 
-			if ( ! connecting )
-				next();
+		/**
+		 * Handles binding any events
+		 * @private
+		 */
+		function _bindWindowEvents() {
+			_Cache.$window.on( 'blur.wp-heartbeat-focus', _onWindowBlur );
+			_Cache.$window.on( 'focus.wp-heartbeat-focus', _onWindowFocus );
 		}
 
-		function setFrameEvents() {
-			$('iframe').each( function( i, frame ){
-				if ( ! isLocalFrame( frame ) )
+		/**
+		 * Handles performing anything we expect to happen when the window is blurred
+		 *
+		 * @param event
+		 * @private
+		 */
+		function _onWindowBlur( event ) {
+			_bindIframeEvents();
+			_clearWindowBlurTimer();
+
+			// todo: not really sure why this was 500 in the first place, seems hacky? (there wasn't any commenting as to why)
+			_Cache.windowBlurTimer = setTimeout( _userHasBecomeInactive, 500 );
+		}
+
+		/**
+		 * Handles performing anything we expect to happen when the window is focused
+		 *
+		 * @param event
+		 * @private
+		 */
+		function _onWindowFocus( event ) {
+			_unbindIframeEvents();
+			_userHasBecomeActive();
+		}
+
+		/**
+		 * Handles binding any iframe events that are needed for this API to work effectively
+		 *
+		 * @private
+		 */
+		function _bindIframeEvents() {
+			$( document.querySelectorAll( 'iframe' ) ).each( function( i, iframe ) {
+				if ( _isLocalFrame( iframe ) === false ) {
 					return;
+				}
 
-				if ( $.data( frame, 'wp-heartbeat-focus' ) )
+				// if we already set data for this iframe, skip it
+				if ( $.data( iframe, 'wp-heartbeat-focus' ) ) {
 					return;
+				}
 
-				$.data( frame, 'wp-heartbeat-focus', 1 );
+				// set data for this frame now so we know not to set it again next time this is called
+				$.data( iframe, 'wp-heartbeat-focus', true );
 
-				$( frame.contentWindow ).on( 'focus.wp-heartbeat-focus', function(e) {
-					focused();
-				}).on('blur.wp-heartbeat-focus', function(e) {
-					setFrameEvents();
-					frameBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 );
-				});
-			});
+				// cache a reference to the jquery object for this iframe window object
+				var $iframeWindow = $( iframe.contentWindow );
+
+				// now setup a focus event for this iframe
+				$iframeWindow.on( 'focus.wp-heartbeat-focus', _userHasBecomeActive );
+				$iframeWindow.on( 'blur.wp-heartbeat-focus', _onIframeBlur );
+
+			} );
 		}
 
-		$(window).on( 'blur.wp-heartbeat-focus', function(e) {
-			setFrameEvents();
-			winBlurTimeout = window.setTimeout( function(){ blurred(); }, 500 );
-		}).on( 'focus.wp-heartbeat-focus', function() {
-			$('iframe').each( function( i, frame ) {
-				if ( !isLocalFrame( frame ) )
+		/**
+		 * Callback for when an iframe becomes blurred on the page
+		 *
+		 * @param event
+		 * @private
+		 */
+		function _onIframeBlur( event ) {
+			_bindIframeEvents();
+			_clearFrameBlurTimer();
+
+			// todo: not really sure why this was 500 in the first place, seems hacky? (there wasn't any commenting as to why)
+			_Cache.frameBlurTimer = setTimeout( _userHasBecomeInactive, 500 );
+		}
+
+		/**
+		 * Unbinds any previously bound heartbeat focus events from all iframes on the page (if they belong to us)
+		 *
+		 * @private
+		 */
+		function _unbindIframeEvents() {
+			$( document.querySelectorAll( 'iframe' ) ).each( function( i, iframe ) {
+				if ( _isLocalFrame( iframe ) === false ) {
 					return;
+				}
 
-				$.removeData( frame, 'wp-heartbeat-focus' );
-				$( frame.contentWindow ).off( '.wp-heartbeat-focus' );
-			});
+				$.removeData( iframe, 'wp-heartbeat-focus' );
+				$( iframe.contentWindow ).off( '.wp-heartbeat-focus' );
+			} );
+		}
 
-			focused();
-		});
+		/**
+		 * Performs the next tick of the timer for our heartbeat. Verifies that we're supposed to continue before trying
+		 * to start the next tick.
+		 *
+		 * @private
+		 */
+		function _nextTick() {
+			var timeSinceLastTick = _getUnixTimestamp() - _Settings.lastHeartbeatTick;
+			var timeLimit = _Settings.heartbeatInterval;
 
-		function userIsActive() {
-			userActiveEvents = false;
-			$(document).off( '.wp-heartbeat-active' );
-			$('iframe').each( function( i, frame ) {
-				if ( ! isLocalFrame( frame ) )
+			// make sure we're running before doing anything - we don't want anything crazy happening here
+			if ( _Settings.isRunning === false ) {
+				return;
+			}
+
+			// normalize the timeLimit
+			if ( _hasFocus() === false ) {
+				// set the time limit to 2 minutes because we don't have focus
+				timeLimit = 120000;
+			} else if ( _Settings.countDown > 0 && _Settings.temporaryHeartbeatInterval > 0 ) {
+				timeLimit = _Settings.temporaryHeartbeatInterval;
+			}
+
+			_clearHeartbeatTimer();
+
+			// check to see if we need to connect now or later and then set things up to do just that
+			if ( timeSinceLastTick < timeLimit ) {
+				_Cache.heartbeatTimer = setTimeout( _connect, timeLimit - timeSinceLastTick );
+			} else {
+				_connect();
+			}
+		}
+
+		/**
+		 * Handles unbinding any events that were previously bound through `_bindUserActivityEvents()`
+		 *
+		 * @private
+		 */
+		function _unbindUserActivityEvents() {
+			// remove the mouseover event as we want to stop it from doing anything
+			_Cache.$document.off( '.wp-heartbeat-active' );
+
+			// loop through all iframes on the page and if they are local iframes, remove the previously attached events
+			$( document.querySelectorAll( 'iframe' ) ).each( function( i, iframe ) {
+
+				// make sure this frame is one of ours before trying to access it's contentWindow
+				if ( _isLocalFrame( iframe ) === false ) {
 					return;
+				}
 
-				$( frame.contentWindow ).off( '.wp-heartbeat-active' );
-			});
+				// remove the iframes heartbeat events
+				$( iframe.contentWindow ).off( '.wp-heartbeat-active' );
 
-			focused();
+			} );
 		}
 
-		// Set 'hasFocus = true' if user is active and the window is in the background.
-		// Set 'hasFocus = false' if the user has been inactive (no mouse or keyboard activity) for 5 min. even when the window has focus.
-		function checkUserActive() {
-			var lastActive = isUserActive ? time() - isUserActive : 0;
+		/**
+		 * Handles binding any events necessary to mark the user as active again
+		 *
+		 * @private
+		 */
+		function _bindUserActivityEvents() {
+			// when the user moves their mouse, mark the the user as becoming active again
+			_Cache.$document.on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', _userHasBecomeActive );
 
-			// Throttle down when no mouse or keyboard activity for 5 min
-			if ( lastActive > 300000 && hasFocus )
-				 blurred();
+			// make sure we handle iframes too
+			$( document.querySelectorAll( 'iframe' ) ).each( function( i, iframe ) {
 
-			if ( ! userActiveEvents ) {
-				$(document).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } );
+				// make sure this frame is one of ours before trying to access it's contentWindow
+				if ( _isLocalFrame( iframe ) === false ) {
+					return;
+				}
 
-				$('iframe').each( function( i, frame ) {
-					if ( ! isLocalFrame( frame ) )
-						return;
+				// bind a mouseover event to this iframe's window object so we know when the user has become active again
+				$( iframe.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', _userHasBecomeActive );
 
-					$( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active', function(){ userIsActive(); } );
-				});
+			} );
+		}
 
-				userActiveEvents = true;
+		/**
+		 * Periodically called to ensure that the user is still active. If the user becomes inactive, we take action.
+		 *
+		 * @private
+		 */
+		function _checkIfUserIsStillActive() {
+			var timeSinceUserWasLastActive = 0;
+			if ( _Settings.userIsActive === true ) {
+				timeSinceUserWasLastActive = _getUnixTimestamp() - _Settings.lastUserActivityTimestamp;
 			}
+
+			// check to see if the user has become inactive or not ( no activity in 5 minutes )
+			if ( timeSinceUserWasLastActive > 300000 && _hasFocus() === true ) {
+				_userHasBecomeInactive();
+				_bindUserActivityEvents();
+			}
 		}
 
-		// Check for user activity every 30 seconds.
-		window.setInterval( function(){ checkUserActive(); }, 30000 );
+		/**
+		 * Starts the heartbeat pulse if it isn't already running
+		 * todo: maybe add a trigger() here for other applications that need to be informed of when heartbeat starts
+		 *
+		 * @returns {boolean}
+		 * @private
+		 */
+		function _start() {
+			if ( _Settings.isRunning === true ) {
+				return false;
+			}
 
-		if ( this.autostart ) {
-			$(document).ready( function() {
-				// Start one tick (15 sec) after DOM ready
-				running = true;
-				tick = time();
-				next();
-			});
+			_connect();
+
+			_Settings.isRunning = true;
+			return true;
 		}
 
-		this.hasFocus = function() {
-			return hasFocus;
+		/**
+		 * Stops the heartbeat pulse if isn't already running
+		 * todo: should this be clearing timers as well? ( _clearWindowBlurTimer(), _clearFrameBlurTimer() and _clearHeartbeatTimer() )
+		 *
+		 * @returns {boolean}
+		 * @private
+		 */
+		function _stop() {
+			if ( _Cache.ajaxRequest !== null && _Cache.ajaxRequest.readyState !== 4 ) {
+				_Cache.ajaxRequest.abort();
+				_Cache.ajaxRequest = null;
+			}
+
+			// todo: this wasn't passing anything previously. I feel like it should be passing 'abort'
+			_triggerError();
+
+			// update our internal value for whether or not we are currently running heartbeat
+			_Settings.isRunning = false;
+
+			return true;
 		}
 
 		/**
-		 * Get/Set the interval
+		 * Gets or sets the current heartbeat interval based on speed and ticks passed to it. If no speed is passed, the
+		 * function simply returns the current interval.
 		 *
 		 * When setting to 'fast', the interval is 5 sec. for the next 30 ticks (for 2 min and 30 sec).
 		 * If the window doesn't have focus, the interval slows down to 2 min.
 		 *
-		 * @param string speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec)
-		 * @param string ticks Used with speed = 'fast', how many ticks before the speed reverts back
-		 * @return int Current interval in seconds
+		 * @param speed Interval speed: 'fast' (5sec), 'standard' (15sec) default, 'slow' (60sec)
+		 * @param ticks Used with speed = 'fast', how many ticks before the speed reverts back
+		 * @returns {number} interval in seconds
+		 * @private
 		 */
-		this.interval = function( speed, ticks ) {
-			var reset, seconds;
-			ticks = parseInt( ticks, 10 ) || 30;
-			ticks = ticks < 1 || ticks > 30 ? 30 : ticks;
+		function _interval( speed, ticks ) {
+			var reset, seconds = 15;
 
-			if ( speed ) {
-				switch ( speed ) {
-					case 'fast':
-						seconds = 5;
-						countdown = ticks;
-						break;
-					case 'slow':
-						seconds = 60;
-						countdown = 0;
-						break;
-					case 'long-polling':
-						// Allow long polling, (experimental)
-						interval = 0;
-						return 0;
-						break;
-					default:
-						seconds = 15;
-						countdown = 0;
+			if ( speed === undefined ) {
+				if ( _hasFocus() === false ) {
+					return 120;
 				}
 
-				// Reset when the new interval value is lower than the current one
-				reset = seconds * 1000 < interval;
+				// return the existing values
+				return _Settings.temporaryHeartbeatInterval ? ( _Settings.temporaryHeartbeatInterval / 1000 ) : ( _Settings.heartbeatInterval * 1000 );
+			}
 
-				if ( countdown > 0 ) {
-					tempInterval = seconds * 1000;
-				} else {
-					interval = seconds * 1000;
-					tempInterval = 0;
-				}
+			// normalize ticks before continuing
+			ticks = parseInt( ticks, 10 ) || 30;
 
-				if ( reset )
-					next();
+			// make sure `ticks` is within bounds
+			if ( ticks < 1 || ticks > 30 ) {
+				ticks = 30;
 			}
 
-			if ( ! hasFocus )
+			// convert speed from a string if necessary - 'long-polling' is currently experimental
+			if ( speed === 'fast' ) {
+				seconds = 5;
+				_Settings.countDown = ticks;
+			} else if ( speed === 'slow' ) {
+				seconds = 60;
+				_Settings.countDown = 0;
+			} else if ( speed === 'long-polling' ) {
+				return _Settings.heartbeatInterval = 0;
+			}
+
+			// determine whether or not we should reset based on whether the new interval value is lower than the current
+			// one or not
+			reset = seconds * 1000 < _Settings.heartbeatInterval;
+
+			if ( _Settings.countDown > 0 ) {
+				_Settings.temporaryHeartbeatInterval = seconds * 1000;
+			} else {
+				_Settings.heartbeatInterval = seconds * 1000;
+				_Settings.temporaryHeartbeatInterval = 0;
+			}
+
+			if ( reset === true ) {
+				_nextTick();
+			}
+
+			// check to see if we have focus or not
+			if ( _hasFocus() === false ) {
 				return 120;
+			}
 
-			return tempInterval ? tempInterval / 1000 : interval / 1000;
-		};
+			// return the new values
+			return _Settings.temporaryHeartbeatInterval ? ( _Settings.temporaryHeartbeatInterval / 1000 ) : ( _Settings.heartbeatInterval * 1000 );
+		}
 
-		// Start. Has no effect if heartbeat is already running
-		this.start = function() {
-			if ( running )
-				return false;
+		/**
+		 * Returns a boolean that indicates whether or not heartbeat has focus at the moment
+		 *
+		 * @returns boolean
+		 * @private
+		 */
+		function _hasFocus() {
+			return _Settings.hasFocus;
+		}
 
-			running = true;
-			connect();
-			return true;
-		};
+		/**
+		 * Handles the actions to take when the user has become inactive
+		 * todo: possibly add a $document.trigger() here to notify other applications?
+		 *
+		 * @private
+		 */
+		function _userHasBecomeInactive() {
+			_clearWindowBlurTimer();
+			_clearFrameBlurTimer();
+			_Settings.hasFocus = false;
+		}
 
-		// Stop. If a XHR is in progress, abort it
-		this.stop = function() {
-			if ( self.xhr && self.xhr.readyState != 4 )
-				self.xhr.abort();
+		/**
+		 * Handles the actions to take when the heartbeat window becomes focused.
+		 * todo: possibly add a $document.trigger() here to notify other applications?
+		 *
+		 * @private
+		 */
+		function _userHasBecomeActive() {
+			_clearWindowBlurTimer();
+			_clearFrameBlurTimer();
+			_unbindUserActivityEvents();
+			_updateUsersLastActivityTime();
 
-			// Reset the error state
-			errorstate();
-			running = false;
-			return true;
+			if ( _hasFocus() === true ) {
+				return;
+			}
+
+			_Settings.hasFocus = true;
+			_clearHeartbeatTimer();
+
+			if ( _Settings.isConnecting === false ) {
+				_nextTick();
+			}
 		}
 
 		/**
-		 * Enqueue data to send with the next XHR
+		 * Updates the user's last activity timestamp
+		 * todo: perhaps trigger an event to notify other applications?
 		 *
-		 * As the data is sent later, this function doesn't return the XHR response.
-		 * To see the response, use the custom jQuery event 'heartbeat-tick' on the document, example:
-		 *		$(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) {
-		 *			// code
-		 *		});
-		 * If the same 'handle' is used more than once, the data is not overwritten when the third argument is 'true'.
-		 * Use wp.heartbeat.isQueued('handle') to see if any data is already queued for that handle.
+		 * @private
+		 */
+		function _updateUsersLastActivityTime() {
+			_Settings.lastUserActivityTimestamp = _getUnixTimestamp();
+		}
+
+		/**
+		 * Clears the window blur timer if it exists
 		 *
-		 * $param string handle Unique handle for the data. The handle is used in PHP to receive the data.
-		 * $param mixed data The data to send.
-		 * $param bool dont_overwrite Whether to overwrite existing data in the queue.
-		 * $return bool Whether the data was queued or not.
+		 * @private
 		 */
-		this.enqueue = function( handle, data, dont_overwrite ) {
-			if ( handle ) {
-				if ( queue.hasOwnProperty( handle ) && dont_overwrite )
-					return false;
+		function _clearWindowBlurTimer() {
+			if ( _Cache.windowBlurTimer !== null ) {
+				clearTimeout( _Cache.windowBlurTimer );
+				_Cache.windowBlurTimer = null;
+			}
+		}
 
-				queue[handle] = data;
-				return true;
+		/**
+		 * Clears the frame blur timer if it exists
+		 *
+		 * @private
+		 */
+		function _clearFrameBlurTimer() {
+			if ( _Cache.frameBlurTimer !== null ) {
+				clearTimeout( _Cache.frameBlurTimer );
+				_Cache.frameBlurTimer = null;
 			}
-			return false;
 		}
 
 		/**
-		 * Check if data with a particular handle is queued
+		 * Clears the heartbeat timer
 		 *
-		 * $param string handle The handle for the data
-		 * $return mixed The data queued with that handle or null
+		 * @private
 		 */
-		this.isQueued = function( handle ) {
-			return queue[handle];
+		function _clearHeartbeatTimer() {
+			if ( _Cache.heartbeatTimer !== null ) {
+				clearTimeout( _Cache.heartbeatTimer );
+				_Cache.heartbeatTimer = null;
+			}
 		}
+
+		/**
+		 * Call our object initializer to make sure things get setup properly before this object is used
+		 */
+		_initialize();
+
+		/**
+		 * Explicitly expose any methods we want to be available to the public scope
+		 */
+		return {
+			shouldAutoStart : _shouldAutoStart,
+			hasActiveConnection : _hasActiveConnection,
+			isLocalFrame : _isLocalFrame,
+			enqueueData : _enqueueData,
+			dataIsQueued : _dataIsQueued,
+			start : _start,
+			stop : _stop,
+			interval : _interval,
+			hasFocus : _hasFocus
+		};
 	}
 
-	$.extend( Heartbeat.prototype, {
-		tick: function( data, textStatus, jqXHR ) {
-			$(document).trigger( 'heartbeat-tick', [data, textStatus, jqXHR] );
-		},
-		error: function( jqXHR, textStatus, error ) {
-			$(document).trigger( 'heartbeat-error', [jqXHR, textStatus, error] );
-		}
-	});
+	// ensure our global `wp` object exists
+	window.wp = window.wp || {};
 
-	wp.heartbeat = new Heartbeat();
+	// create our new heartbeat object and expose it to the public
+	window.wp.heartbeat = new Heartbeat();
 
-}(jQuery));
+} )( window, jQuery );
\ No newline at end of file
