heartbeat.js 23 KB

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