heartbeat.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874
  1. /**
  2. * Heartbeat API
  3. *
  4. * Heartbeat is a simple server polling API that sends XHR requests to
  5. * the server every 15 - 60 seconds and triggers events (or callbacks) upon
  6. * receiving data. Currently these 'ticks' handle transports for post locking,
  7. * login-expiration warnings, autosave, and related tasks while a user is logged in.
  8. *
  9. * Available PHP filters (in ajax-actions.php):
  10. * - heartbeat_received
  11. * - heartbeat_send
  12. * - heartbeat_tick
  13. * - heartbeat_nopriv_received
  14. * - heartbeat_nopriv_send
  15. * - heartbeat_nopriv_tick
  16. * @see wp_ajax_nopriv_heartbeat(), wp_ajax_heartbeat()
  17. *
  18. * Custom jQuery events:
  19. * - heartbeat-send
  20. * - heartbeat-tick
  21. * - heartbeat-error
  22. * - heartbeat-connection-lost
  23. * - heartbeat-connection-restored
  24. * - heartbeat-nonces-expired
  25. *
  26. * @since 3.6.0
  27. * @output wp-includes/js/heartbeat.js
  28. */
  29. ( function( $, window, undefined ) {
  30. /**
  31. * Constructs the Heartbeat API.
  32. *
  33. * @since 3.6.0
  34. *
  35. * @return {Object} An instance of the Heartbeat class.
  36. * @constructor
  37. */
  38. var Heartbeat = function() {
  39. var $document = $(document),
  40. settings = {
  41. // Suspend/resume.
  42. suspend: false,
  43. // Whether suspending is enabled.
  44. suspendEnabled: true,
  45. // Current screen id, defaults to the JS global 'pagenow' when present
  46. // (in the admin) or 'front'.
  47. screenId: '',
  48. // XHR request URL, defaults to the JS global 'ajaxurl' when present.
  49. url: '',
  50. // Timestamp, start of the last connection request.
  51. lastTick: 0,
  52. // Container for the enqueued items.
  53. queue: {},
  54. // Connect interval (in seconds).
  55. mainInterval: 60,
  56. // Used when the interval is set to 5 seconds temporarily.
  57. tempInterval: 0,
  58. // Used when the interval is reset.
  59. originalInterval: 0,
  60. // Used to limit the number of Ajax requests.
  61. minimalInterval: 0,
  62. // Used together with tempInterval.
  63. countdown: 0,
  64. // Whether a connection is currently in progress.
  65. connecting: false,
  66. // Whether a connection error occurred.
  67. connectionError: false,
  68. // Used to track non-critical errors.
  69. errorcount: 0,
  70. // Whether at least one connection has been completed successfully.
  71. hasConnected: false,
  72. // Whether the current browser window is in focus and the user is active.
  73. hasFocus: true,
  74. // Timestamp, last time the user was active. Checked every 30 seconds.
  75. userActivity: 0,
  76. // Flag whether events tracking user activity were set.
  77. userActivityEvents: false,
  78. // Timer that keeps track of how long a user has focus.
  79. checkFocusTimer: 0,
  80. // Timer that keeps track of how long needs to be waited before connecting to
  81. // the server again.
  82. beatTimer: 0
  83. };
  84. /**
  85. * Sets local variables and events, then starts the heartbeat.
  86. *
  87. * @since 3.8.0
  88. * @access private
  89. *
  90. * @return {void}
  91. */
  92. function initialize() {
  93. var options, hidden, visibilityState, visibilitychange;
  94. if ( typeof window.pagenow === 'string' ) {
  95. settings.screenId = window.pagenow;
  96. }
  97. if ( typeof window.ajaxurl === 'string' ) {
  98. settings.url = window.ajaxurl;
  99. }
  100. // Pull in options passed from PHP.
  101. if ( typeof window.heartbeatSettings === 'object' ) {
  102. options = window.heartbeatSettings;
  103. // The XHR URL can be passed as option when window.ajaxurl is not set.
  104. if ( ! settings.url && options.ajaxurl ) {
  105. settings.url = options.ajaxurl;
  106. }
  107. /*
  108. * The interval can be from 15 to 120 seconds and can be set temporarily to 5 seconds.
  109. * It can be set in the initial options or changed later through JS and/or through PHP.
  110. */
  111. if ( options.interval ) {
  112. settings.mainInterval = options.interval;
  113. if ( settings.mainInterval < 15 ) {
  114. settings.mainInterval = 15;
  115. } else if ( settings.mainInterval > 120 ) {
  116. settings.mainInterval = 120;
  117. }
  118. }
  119. /*
  120. * Used to limit the number of Ajax requests. Overrides all other intervals
  121. * if they are shorter. Needed for some hosts that cannot handle frequent requests
  122. * and the user may exceed the allocated server CPU time, etc. The minimal interval
  123. * can be up to 600 seconds, however setting it to longer than 120 seconds
  124. * will limit or disable some of the functionality (like post locks).
  125. * Once set at initialization, minimalInterval cannot be changed/overridden.
  126. */
  127. if ( options.minimalInterval ) {
  128. options.minimalInterval = parseInt( options.minimalInterval, 10 );
  129. settings.minimalInterval = options.minimalInterval > 0 && options.minimalInterval <= 600 ? options.minimalInterval : 0;
  130. }
  131. if ( settings.minimalInterval && settings.mainInterval < settings.minimalInterval ) {
  132. settings.mainInterval = settings.minimalInterval;
  133. }
  134. // 'screenId' can be added from settings on the front end where the JS global
  135. // 'pagenow' is not set.
  136. if ( ! settings.screenId ) {
  137. settings.screenId = options.screenId || 'front';
  138. }
  139. if ( options.suspension === 'disable' ) {
  140. settings.suspendEnabled = false;
  141. }
  142. }
  143. // Convert to milliseconds.
  144. settings.mainInterval = settings.mainInterval * 1000;
  145. settings.originalInterval = settings.mainInterval;
  146. if ( settings.minimalInterval ) {
  147. settings.minimalInterval = settings.minimalInterval * 1000;
  148. }
  149. /*
  150. * Switch the interval to 120 seconds by using the Page Visibility API.
  151. * If the browser doesn't support it (Safari < 7, Android < 4.4, IE < 10), the
  152. * interval will be increased to 120 seconds after 5 minutes of mouse and keyboard
  153. * inactivity.
  154. */
  155. if ( typeof document.hidden !== 'undefined' ) {
  156. hidden = 'hidden';
  157. visibilitychange = 'visibilitychange';
  158. visibilityState = 'visibilityState';
  159. } else if ( typeof document.msHidden !== 'undefined' ) { // IE10.
  160. hidden = 'msHidden';
  161. visibilitychange = 'msvisibilitychange';
  162. visibilityState = 'msVisibilityState';
  163. } else if ( typeof document.webkitHidden !== 'undefined' ) { // Android.
  164. hidden = 'webkitHidden';
  165. visibilitychange = 'webkitvisibilitychange';
  166. visibilityState = 'webkitVisibilityState';
  167. }
  168. if ( hidden ) {
  169. if ( document[hidden] ) {
  170. settings.hasFocus = false;
  171. }
  172. $document.on( visibilitychange + '.wp-heartbeat', function() {
  173. if ( document[visibilityState] === 'hidden' ) {
  174. blurred();
  175. window.clearInterval( settings.checkFocusTimer );
  176. } else {
  177. focused();
  178. if ( document.hasFocus ) {
  179. settings.checkFocusTimer = window.setInterval( checkFocus, 10000 );
  180. }
  181. }
  182. });
  183. }
  184. // Use document.hasFocus() if available.
  185. if ( document.hasFocus ) {
  186. settings.checkFocusTimer = window.setInterval( checkFocus, 10000 );
  187. }
  188. $(window).on( 'unload.wp-heartbeat', function() {
  189. // Don't connect anymore.
  190. settings.suspend = true;
  191. // Abort the last request if not completed.
  192. if ( settings.xhr && settings.xhr.readyState !== 4 ) {
  193. settings.xhr.abort();
  194. }
  195. });
  196. // Check for user activity every 30 seconds.
  197. window.setInterval( checkUserActivity, 30000 );
  198. // Start one tick after DOM ready.
  199. $( function() {
  200. settings.lastTick = time();
  201. scheduleNextTick();
  202. });
  203. }
  204. /**
  205. * Returns the current time according to the browser.
  206. *
  207. * @since 3.6.0
  208. * @access private
  209. *
  210. * @return {number} Returns the current time.
  211. */
  212. function time() {
  213. return (new Date()).getTime();
  214. }
  215. /**
  216. * Checks if the iframe is from the same origin.
  217. *
  218. * @since 3.6.0
  219. * @access private
  220. *
  221. * @return {boolean} Returns whether or not the iframe is from the same origin.
  222. */
  223. function isLocalFrame( frame ) {
  224. var origin, src = frame.src;
  225. /*
  226. * Need to compare strings as WebKit doesn't throw JS errors when iframes have
  227. * different origin. It throws uncatchable exceptions.
  228. */
  229. if ( src && /^https?:\/\//.test( src ) ) {
  230. origin = window.location.origin ? window.location.origin : window.location.protocol + '//' + window.location.host;
  231. if ( src.indexOf( origin ) !== 0 ) {
  232. return false;
  233. }
  234. }
  235. try {
  236. if ( frame.contentWindow.document ) {
  237. return true;
  238. }
  239. } catch(e) {}
  240. return false;
  241. }
  242. /**
  243. * Checks if the document's focus has changed.
  244. *
  245. * @since 4.1.0
  246. * @access private
  247. *
  248. * @return {void}
  249. */
  250. function checkFocus() {
  251. if ( settings.hasFocus && ! document.hasFocus() ) {
  252. blurred();
  253. } else if ( ! settings.hasFocus && document.hasFocus() ) {
  254. focused();
  255. }
  256. }
  257. /**
  258. * Sets error state and fires an event on XHR errors or timeout.
  259. *
  260. * @since 3.8.0
  261. * @access private
  262. *
  263. * @param {string} error The error type passed from the XHR.
  264. * @param {number} status The HTTP status code passed from jqXHR
  265. * (200, 404, 500, etc.).
  266. *
  267. * @return {void}
  268. */
  269. function setErrorState( error, status ) {
  270. var trigger;
  271. if ( error ) {
  272. switch ( error ) {
  273. case 'abort':
  274. // Do nothing.
  275. break;
  276. case 'timeout':
  277. // No response for 30 seconds.
  278. trigger = true;
  279. break;
  280. case 'error':
  281. if ( 503 === status && settings.hasConnected ) {
  282. trigger = true;
  283. break;
  284. }
  285. /* falls through */
  286. case 'parsererror':
  287. case 'empty':
  288. case 'unknown':
  289. settings.errorcount++;
  290. if ( settings.errorcount > 2 && settings.hasConnected ) {
  291. trigger = true;
  292. }
  293. break;
  294. }
  295. if ( trigger && ! hasConnectionError() ) {
  296. settings.connectionError = true;
  297. $document.trigger( 'heartbeat-connection-lost', [error, status] );
  298. wp.hooks.doAction( 'heartbeat.connection-lost', error, status );
  299. }
  300. }
  301. }
  302. /**
  303. * Clears the error state and fires an event if there is a connection error.
  304. *
  305. * @since 3.8.0
  306. * @access private
  307. *
  308. * @return {void}
  309. */
  310. function clearErrorState() {
  311. // Has connected successfully.
  312. settings.hasConnected = true;
  313. if ( hasConnectionError() ) {
  314. settings.errorcount = 0;
  315. settings.connectionError = false;
  316. $document.trigger( 'heartbeat-connection-restored' );
  317. wp.hooks.doAction( 'heartbeat.connection-restored' );
  318. }
  319. }
  320. /**
  321. * Gathers the data and connects to the server.
  322. *
  323. * @since 3.6.0
  324. * @access private
  325. *
  326. * @return {void}
  327. */
  328. function connect() {
  329. var ajaxData, heartbeatData;
  330. // If the connection to the server is slower than the interval,
  331. // heartbeat connects as soon as the previous connection's response is received.
  332. if ( settings.connecting || settings.suspend ) {
  333. return;
  334. }
  335. settings.lastTick = time();
  336. heartbeatData = $.extend( {}, settings.queue );
  337. // Clear the data queue. Anything added after this point will be sent on the next tick.
  338. settings.queue = {};
  339. $document.trigger( 'heartbeat-send', [ heartbeatData ] );
  340. wp.hooks.doAction( 'heartbeat.send', heartbeatData );
  341. ajaxData = {
  342. data: heartbeatData,
  343. interval: settings.tempInterval ? settings.tempInterval / 1000 : settings.mainInterval / 1000,
  344. _nonce: typeof window.heartbeatSettings === 'object' ? window.heartbeatSettings.nonce : '',
  345. action: 'heartbeat',
  346. screen_id: settings.screenId,
  347. has_focus: settings.hasFocus
  348. };
  349. if ( 'customize' === settings.screenId ) {
  350. ajaxData.wp_customize = 'on';
  351. }
  352. settings.connecting = true;
  353. settings.xhr = $.ajax({
  354. url: settings.url,
  355. type: 'post',
  356. timeout: 30000, // Throw an error if not completed after 30 seconds.
  357. data: ajaxData,
  358. dataType: 'json'
  359. }).always( function() {
  360. settings.connecting = false;
  361. scheduleNextTick();
  362. }).done( function( response, textStatus, jqXHR ) {
  363. var newInterval;
  364. if ( ! response ) {
  365. setErrorState( 'empty' );
  366. return;
  367. }
  368. clearErrorState();
  369. if ( response.nonces_expired ) {
  370. $document.trigger( 'heartbeat-nonces-expired' );
  371. wp.hooks.doAction( 'heartbeat.nonces-expired' );
  372. }
  373. // Change the interval from PHP.
  374. if ( response.heartbeat_interval ) {
  375. newInterval = response.heartbeat_interval;
  376. delete response.heartbeat_interval;
  377. }
  378. // Update the heartbeat nonce if set.
  379. if ( response.heartbeat_nonce && typeof window.heartbeatSettings === 'object' ) {
  380. window.heartbeatSettings.nonce = response.heartbeat_nonce;
  381. delete response.heartbeat_nonce;
  382. }
  383. // Update the Rest API nonce if set and wp-api loaded.
  384. if ( response.rest_nonce && typeof window.wpApiSettings === 'object' ) {
  385. window.wpApiSettings.nonce = response.rest_nonce;
  386. // This nonce is required for api-fetch through heartbeat.tick.
  387. // delete response.rest_nonce;
  388. }
  389. $document.trigger( 'heartbeat-tick', [response, textStatus, jqXHR] );
  390. wp.hooks.doAction( 'heartbeat.tick', response, textStatus, jqXHR );
  391. // Do this last. Can trigger the next XHR if connection time > 5 seconds and newInterval == 'fast'.
  392. if ( newInterval ) {
  393. interval( newInterval );
  394. }
  395. }).fail( function( jqXHR, textStatus, error ) {
  396. setErrorState( textStatus || 'unknown', jqXHR.status );
  397. $document.trigger( 'heartbeat-error', [jqXHR, textStatus, error] );
  398. wp.hooks.doAction( 'heartbeat.error', jqXHR, textStatus, error );
  399. });
  400. }
  401. /**
  402. * Schedules the next connection.
  403. *
  404. * Fires immediately if the connection time is longer than the interval.
  405. *
  406. * @since 3.8.0
  407. * @access private
  408. *
  409. * @return {void}
  410. */
  411. function scheduleNextTick() {
  412. var delta = time() - settings.lastTick,
  413. interval = settings.mainInterval;
  414. if ( settings.suspend ) {
  415. return;
  416. }
  417. if ( ! settings.hasFocus ) {
  418. interval = 120000; // 120 seconds. Post locks expire after 150 seconds.
  419. } else if ( settings.countdown > 0 && settings.tempInterval ) {
  420. interval = settings.tempInterval;
  421. settings.countdown--;
  422. if ( settings.countdown < 1 ) {
  423. settings.tempInterval = 0;
  424. }
  425. }
  426. if ( settings.minimalInterval && interval < settings.minimalInterval ) {
  427. interval = settings.minimalInterval;
  428. }
  429. window.clearTimeout( settings.beatTimer );
  430. if ( delta < interval ) {
  431. settings.beatTimer = window.setTimeout(
  432. function() {
  433. connect();
  434. },
  435. interval - delta
  436. );
  437. } else {
  438. connect();
  439. }
  440. }
  441. /**
  442. * Sets the internal state when the browser window becomes hidden or loses focus.
  443. *
  444. * @since 3.6.0
  445. * @access private
  446. *
  447. * @return {void}
  448. */
  449. function blurred() {
  450. settings.hasFocus = false;
  451. }
  452. /**
  453. * Sets the internal state when the browser window becomes visible or is in focus.
  454. *
  455. * @since 3.6.0
  456. * @access private
  457. *
  458. * @return {void}
  459. */
  460. function focused() {
  461. settings.userActivity = time();
  462. // Resume if suspended.
  463. settings.suspend = false;
  464. if ( ! settings.hasFocus ) {
  465. settings.hasFocus = true;
  466. scheduleNextTick();
  467. }
  468. }
  469. /**
  470. * Runs when the user becomes active after a period of inactivity.
  471. *
  472. * @since 3.6.0
  473. * @access private
  474. *
  475. * @return {void}
  476. */
  477. function userIsActive() {
  478. settings.userActivityEvents = false;
  479. $document.off( '.wp-heartbeat-active' );
  480. $('iframe').each( function( i, frame ) {
  481. if ( isLocalFrame( frame ) ) {
  482. $( frame.contentWindow ).off( '.wp-heartbeat-active' );
  483. }
  484. });
  485. focused();
  486. }
  487. /**
  488. * Checks for user activity.
  489. *
  490. * Runs every 30 seconds. Sets 'hasFocus = true' if user is active and the window
  491. * is in the background. Sets 'hasFocus = false' if the user has been inactive
  492. * (no mouse or keyboard activity) for 5 minutes even when the window has focus.
  493. *
  494. * @since 3.8.0
  495. * @access private
  496. *
  497. * @return {void}
  498. */
  499. function checkUserActivity() {
  500. var lastActive = settings.userActivity ? time() - settings.userActivity : 0;
  501. // Throttle down when no mouse or keyboard activity for 5 minutes.
  502. if ( lastActive > 300000 && settings.hasFocus ) {
  503. blurred();
  504. }
  505. // Suspend after 10 minutes of inactivity when suspending is enabled.
  506. // Always suspend after 60 minutes of inactivity. This will release the post lock, etc.
  507. if ( ( settings.suspendEnabled && lastActive > 600000 ) || lastActive > 3600000 ) {
  508. settings.suspend = true;
  509. }
  510. if ( ! settings.userActivityEvents ) {
  511. $document.on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active', function() {
  512. userIsActive();
  513. });
  514. $('iframe').each( function( i, frame ) {
  515. if ( isLocalFrame( frame ) ) {
  516. $( frame.contentWindow ).on( 'mouseover.wp-heartbeat-active keyup.wp-heartbeat-active touchend.wp-heartbeat-active', function() {
  517. userIsActive();
  518. });
  519. }
  520. });
  521. settings.userActivityEvents = true;
  522. }
  523. }
  524. // Public methods.
  525. /**
  526. * Checks whether the window (or any local iframe in it) has focus, or the user
  527. * is active.
  528. *
  529. * @since 3.6.0
  530. * @memberOf wp.heartbeat.prototype
  531. *
  532. * @return {boolean} True if the window or the user is active.
  533. */
  534. function hasFocus() {
  535. return settings.hasFocus;
  536. }
  537. /**
  538. * Checks whether there is a connection error.
  539. *
  540. * @since 3.6.0
  541. *
  542. * @memberOf wp.heartbeat.prototype
  543. *
  544. * @return {boolean} True if a connection error was found.
  545. */
  546. function hasConnectionError() {
  547. return settings.connectionError;
  548. }
  549. /**
  550. * Connects as soon as possible regardless of 'hasFocus' state.
  551. *
  552. * Will not open two concurrent connections. If a connection is in progress,
  553. * will connect again immediately after the current connection completes.
  554. *
  555. * @since 3.8.0
  556. *
  557. * @memberOf wp.heartbeat.prototype
  558. *
  559. * @return {void}
  560. */
  561. function connectNow() {
  562. settings.lastTick = 0;
  563. scheduleNextTick();
  564. }
  565. /**
  566. * Disables suspending.
  567. *
  568. * Should be used only when Heartbeat is performing critical tasks like
  569. * autosave, post-locking, etc. Using this on many screens may overload
  570. * the user's hosting account if several browser windows/tabs are left open
  571. * for a long time.
  572. *
  573. * @since 3.8.0
  574. *
  575. * @memberOf wp.heartbeat.prototype
  576. *
  577. * @return {void}
  578. */
  579. function disableSuspend() {
  580. settings.suspendEnabled = false;
  581. }
  582. /**
  583. * Gets/Sets the interval.
  584. *
  585. * When setting to 'fast' or 5, the interval is 5 seconds for the next 30 ticks
  586. * (for 2 minutes and 30 seconds) by default. In this case the number of 'ticks'
  587. * can be passed as second argument. If the window doesn't have focus,
  588. * the interval slows down to 2 minutes.
  589. *
  590. * @since 3.6.0
  591. *
  592. * @memberOf wp.heartbeat.prototype
  593. *
  594. * @param {string|number} speed Interval: 'fast' or 5, 15, 30, 60, 120.
  595. * Fast equals 5.
  596. * @param {string} ticks Tells how many ticks before the interval reverts
  597. * back. Used with speed = 'fast' or 5.
  598. *
  599. * @return {number} Current interval in seconds.
  600. */
  601. function interval( speed, ticks ) {
  602. var newInterval,
  603. oldInterval = settings.tempInterval ? settings.tempInterval : settings.mainInterval;
  604. if ( speed ) {
  605. switch ( speed ) {
  606. case 'fast':
  607. case 5:
  608. newInterval = 5000;
  609. break;
  610. case 15:
  611. newInterval = 15000;
  612. break;
  613. case 30:
  614. newInterval = 30000;
  615. break;
  616. case 60:
  617. newInterval = 60000;
  618. break;
  619. case 120:
  620. newInterval = 120000;
  621. break;
  622. case 'long-polling':
  623. // Allow long polling (experimental).
  624. settings.mainInterval = 0;
  625. return 0;
  626. default:
  627. newInterval = settings.originalInterval;
  628. }
  629. if ( settings.minimalInterval && newInterval < settings.minimalInterval ) {
  630. newInterval = settings.minimalInterval;
  631. }
  632. if ( 5000 === newInterval ) {
  633. ticks = parseInt( ticks, 10 ) || 30;
  634. ticks = ticks < 1 || ticks > 30 ? 30 : ticks;
  635. settings.countdown = ticks;
  636. settings.tempInterval = newInterval;
  637. } else {
  638. settings.countdown = 0;
  639. settings.tempInterval = 0;
  640. settings.mainInterval = newInterval;
  641. }
  642. /*
  643. * Change the next connection time if new interval has been set.
  644. * Will connect immediately if the time since the last connection
  645. * is greater than the new interval.
  646. */
  647. if ( newInterval !== oldInterval ) {
  648. scheduleNextTick();
  649. }
  650. }
  651. return settings.tempInterval ? settings.tempInterval / 1000 : settings.mainInterval / 1000;
  652. }
  653. /**
  654. * Enqueues data to send with the next XHR.
  655. *
  656. * As the data is send asynchronously, this function doesn't return the XHR
  657. * response. To see the response, use the custom jQuery event 'heartbeat-tick'
  658. * on the document, example:
  659. * $(document).on( 'heartbeat-tick.myname', function( event, data, textStatus, jqXHR ) {
  660. * // code
  661. * });
  662. * If the same 'handle' is used more than once, the data is not overwritten when
  663. * the third argument is 'true'. Use `wp.heartbeat.isQueued('handle')` to see if
  664. * any data is already queued for that handle.
  665. *
  666. * @since 3.6.0
  667. *
  668. * @memberOf wp.heartbeat.prototype
  669. *
  670. * @param {string} handle Unique handle for the data, used in PHP to
  671. * receive the data.
  672. * @param {*} data The data to send.
  673. * @param {boolean} noOverwrite Whether to overwrite existing data in the queue.
  674. *
  675. * @return {boolean} True if the data was queued.
  676. */
  677. function enqueue( handle, data, noOverwrite ) {
  678. if ( handle ) {
  679. if ( noOverwrite && this.isQueued( handle ) ) {
  680. return false;
  681. }
  682. settings.queue[handle] = data;
  683. return true;
  684. }
  685. return false;
  686. }
  687. /**
  688. * Checks if data with a particular handle is queued.
  689. *
  690. * @since 3.6.0
  691. *
  692. * @param {string} handle The handle for the data.
  693. *
  694. * @return {boolean} True if the data is queued with this handle.
  695. */
  696. function isQueued( handle ) {
  697. if ( handle ) {
  698. return settings.queue.hasOwnProperty( handle );
  699. }
  700. }
  701. /**
  702. * Removes data with a particular handle from the queue.
  703. *
  704. * @since 3.7.0
  705. *
  706. * @memberOf wp.heartbeat.prototype
  707. *
  708. * @param {string} handle The handle for the data.
  709. *
  710. * @return {void}
  711. */
  712. function dequeue( handle ) {
  713. if ( handle ) {
  714. delete settings.queue[handle];
  715. }
  716. }
  717. /**
  718. * Gets data that was enqueued with a particular handle.
  719. *
  720. * @since 3.7.0
  721. *
  722. * @memberOf wp.heartbeat.prototype
  723. *
  724. * @param {string} handle The handle for the data.
  725. *
  726. * @return {*} The data or undefined.
  727. */
  728. function getQueuedItem( handle ) {
  729. if ( handle ) {
  730. return this.isQueued( handle ) ? settings.queue[handle] : undefined;
  731. }
  732. }
  733. initialize();
  734. // Expose public methods.
  735. return {
  736. hasFocus: hasFocus,
  737. connectNow: connectNow,
  738. disableSuspend: disableSuspend,
  739. interval: interval,
  740. hasConnectionError: hasConnectionError,
  741. enqueue: enqueue,
  742. dequeue: dequeue,
  743. isQueued: isQueued,
  744. getQueuedItem: getQueuedItem
  745. };
  746. };
  747. /**
  748. * Ensure the global `wp` object exists.
  749. *
  750. * @namespace wp
  751. */
  752. window.wp = window.wp || {};
  753. /**
  754. * Contains the Heartbeat API.
  755. *
  756. * @namespace wp.heartbeat
  757. * @type {Heartbeat}
  758. */
  759. window.wp.heartbeat = new Heartbeat();
  760. }( jQuery, window ));