| @@ -8,8 +8,9 @@ | ||
| 8 | 8 | var WP_Optimize_Heartbeat = function () { |
| 9 | 9 | var $ = jQuery; |
| 10 | 10 | var agent_idle_ttl_in_seconds = 60; // retry after 60 seconds without a response |
| 11 | 11 | var wpo_fallback; |
| 12 | + var _setup = false; | |
| 12 | 13 | |
| 13 | 14 | /** |
| 14 | 15 | * Generate a unique ID to be used as agents IDs |
| 15 | 16 | * |
| @@ -30,52 +31,87 @@ | ||
| 30 | 31 | * |
| 31 | 32 | * @returns {void} |
| 32 | 33 | */ |
| 33 | 34 | function setup() { |
| 34 | - $(document).on('heartbeat-send', function(event, data) { | |
| 35 | - for(var uid in WP_Optimize_Heartbeat_Agents) { | |
| 36 | - if (!WP_Optimize_Heartbeat_Agents[uid].sent) { | |
| 37 | - data[uid] = WP_Optimize_Heartbeat_Agents[uid].command; | |
| 35 | + if (false === _setup) { | |
| 36 | + _setup = true; | |
| 37 | + | |
| 38 | + $(document).on('heartbeat-send', function(event, data) { | |
| 39 | + for(var uid in WP_Optimize_Heartbeat_Agents) { | |
| 40 | + var agent = WP_Optimize_Heartbeat_Agents[uid]; | |
| 38 | 41 | |
| 39 | - WP_Optimize_Heartbeat_Agents[uid].sent = true; | |
| 42 | + if (!agent.sent) { | |
| 43 | + if ('command_data' in agent) { | |
| 44 | + data[uid] = {}; | |
| 45 | + data[uid][agent.command] = agent.command_data; | |
| 46 | + } else { | |
| 47 | + data[uid] = agent.command; | |
| 48 | + } | |
| 49 | + | |
| 50 | + agent.sent_time = new Date().getTime(); | |
| 51 | + agent.sent = true; | |
| 52 | + } | |
| 53 | + | |
| 54 | + // Retry after idle time is passed (no response in X seconds) | |
| 55 | + var seconds = ((new Date()).getTime() - agent.sent_time) / 1000; | |
| 56 | + if (seconds > agent_idle_ttl_in_seconds) { | |
| 57 | + agent.sent = false; | |
| 58 | + } | |
| 40 | 59 | } |
| 60 | + }); | |
| 41 | 61 | |
| 42 | - // Retry after idle time is passed (no response in X seconds) | |
| 43 | - var seconds = ((new Date()).getTime() - WP_Optimize_Heartbeat_Agents[uid].sent_time.getTime()) / 1000; | |
| 44 | - if (seconds > agent_idle_ttl_in_seconds) { | |
| 45 | - WP_Optimize_Heartbeat_Agents[uid].sent = false; | |
| 46 | - } | |
| 47 | - } | |
| 48 | - }); | |
| 62 | + $(document).on('heartbeat-tick', function(event, data) { | |
| 63 | + if ('object' == typeof(data.callbacks)) { | |
| 64 | + for(var uid in data.callbacks) { | |
| 65 | + if (is_wpo_heartbeat(uid)) { | |
| 66 | + var response; | |
| 67 | + try { | |
| 68 | + response = JSON.parse(data.callbacks[uid]); | |
| 69 | + } catch(e) { | |
| 70 | + response = data.callbacks[uid]; | |
| 71 | + } | |
| 49 | 72 | |
| 50 | - $(document).on('heartbeat-tick', function(event, data) { | |
| 51 | - if ('object' == typeof(data.callbacks)) { | |
| 52 | - for(var uid in data.callbacks) { | |
| 53 | - if (is_wpo_heartbeat(uid)) { | |
| 54 | - var response = JSON.parse(data.callbacks[uid]); | |
| 73 | + if ('undefined' != typeof(response.result) && false === response.result && ('undefined' == typeof(response.skip_notice) || false === response.skip_notice)) { | |
| 74 | + wp_optimize.notices.show_notice(response.error_code, response.error_message); | |
| 75 | + } else { | |
| 76 | + if ('undefined' !== typeof(WP_Optimize_Heartbeat_Agents[uid]) && WP_Optimize_Heartbeat_Agents[uid].callback instanceof Function) { | |
| 77 | + WP_Optimize_Heartbeat_Agents[uid].callback(response); | |
| 78 | + } | |
| 79 | + } | |
| 55 | 80 | |
| 56 | - if ('undefined' != typeof(response.result) && false == response.result) { | |
| 57 | - wp_optimize.notices.show_notice(response.error_code, response.error_message); | |
| 58 | - } else { | |
| 59 | - WP_Optimize_Heartbeat_Agents[uid].callback(response); | |
| 81 | + if ('undefined' !== typeof WP_Optimize_Heartbeat_Agents[uid]) { | |
| 82 | + delete WP_Optimize_Heartbeat_Agents[uid]; | |
| 83 | + } | |
| 60 | 84 | } |
| 61 | - | |
| 62 | - delete WP_Optimize_Heartbeat_Agents[uid]; | |
| 63 | 85 | } |
| 64 | 86 | } |
| 87 | + }); | |
| 88 | + | |
| 89 | + if (is_heartbeat_api_disabled()) { | |
| 90 | + wpo_fallback = WP_Optimize_Heartbeat_Fallback(); | |
| 91 | + } else { | |
| 92 | + // Some agents send `_wait:false` because the UI needs to execute that action quickly, `disableSuspend` allows for `connectNow` to trigger a heartbeat instantly | |
| 93 | + wp.heartbeat.disableSuspend(); | |
| 65 | 94 | } |
| 66 | - }); | |
| 67 | - | |
| 68 | - if (is_heartbeat_api_disabled()) { | |
| 69 | - wpo_fallback = WP_Optimize_Heartbeat_Fallback(); | |
| 70 | 95 | } |
| 71 | 96 | } |
| 72 | 97 | |
| 73 | 98 | /** |
| 99 | + * Cancel a group of agents all at once | |
| 100 | + * | |
| 101 | + * @param {array} agents_ids The list of agent ids to cancel | |
| 102 | + */ | |
| 103 | + function cancel_agents(agents_ids) { | |
| 104 | + while(agent_id = agents_ids.shift()) { | |
| 105 | + cancel_agent(agent_id); | |
| 106 | + } | |
| 107 | + } | |
| 108 | + | |
| 109 | + /** | |
| 74 | 110 | * Check if heartbeat action is a WP-Optimize action or something else that we should ignore |
| 75 | 111 | * |
| 76 | 112 | * @param {string} uid The UID of the agent |
| 77 | - * @returns {bool} | |
| 113 | + * @returns {boolean} | |
| 78 | 114 | */ |
| 79 | 115 | function is_wpo_heartbeat(uid) { |
| 80 | 116 | return 0 === uid.indexOf('wpo-heartbeat-'); |
| 81 | 117 | } |
| @@ -82,12 +118,12 @@ | ||
| 82 | 118 | |
| 83 | 119 | /** |
| 84 | 120 | * Check if native heartbeat API is available |
| 85 | 121 | * |
| 86 | - * @returns {bool} | |
| 122 | + * @returns {boolean} | |
| 87 | 123 | */ |
| 88 | 124 | function is_heartbeat_api_disabled() { |
| 89 | - return 'undefined' == typeof(wp.heartbeat); | |
| 125 | + return 'undefined' === typeof(wp.heartbeat); | |
| 90 | 126 | } |
| 91 | 127 | |
| 92 | 128 | /** |
| 93 | 129 | * Filter function to check if an agent is already scheduled |
| @@ -93,49 +129,78 @@ | ||
| 93 | 129 | * Filter function to check if an agent is already scheduled |
| 94 | 130 | * |
| 95 | 131 | * @param {object} agent1 First comparison agent, usually already scheduled agents |
| 96 | 132 | * @param {object} agent2 Second comparison agent, usually the one you are trying to check if it already exists |
| 97 | - * @returns {bool} | |
| 133 | + * @returns {boolean} | |
| 98 | 134 | */ |
| 99 | 135 | function do_agents_match(agent1, agent2) { |
| 100 | 136 | var command_matches = agent1.command === agent2.command; |
| 137 | + var subaction1 = agent1.command_data && agent1.command_data.subaction ? agent1.command_data.subaction : undefined; | |
| 138 | + var subaction2 = agent2.command_data && agent2.command_data.subaction ? agent2.command_data.subaction : undefined; | |
| 139 | + var subaction_matches = subaction1 === subaction2; | |
| 101 | 140 | var command_not_sent_yet = false === agent1.sent; |
| 102 | 141 | |
| 103 | - return command_matches && command_not_sent_yet; | |
| 142 | + return command_matches && subaction_matches && command_not_sent_yet; | |
| 104 | 143 | } |
| 105 | 144 | |
| 106 | 145 | /** |
| 107 | 146 | * Add a heartbeat agent that will be sent to backend and has a callback to receive the response |
| 108 | 147 | * |
| 109 | - * @param {object} data Expected an object like {command: string} | |
| 148 | + * @param {object} data Expected an object like {command: string}. Commands will be treated as `data._unique:true` by default. Some commands may need permission to schedule multiple times, by sending `_unique:false` | |
| 110 | 149 | * @returns {string|null} |
| 111 | 150 | */ |
| 112 | 151 | function add_agent(data) { |
| 113 | 152 | var already_scheduled = Object.values(WP_Optimize_Heartbeat_Agents).some(function(agent) { return do_agents_match(agent, data); }); |
| 114 | - if (already_scheduled) { | |
| 153 | + if (already_scheduled && ('undefined' == typeof(data._unique) || (true == data._unique))) { | |
| 115 | 154 | return null; |
| 116 | 155 | } |
| 117 | 156 | |
| 118 | 157 | var agent_id = 'wpo-heartbeat-' + guid(); |
| 119 | 158 | data.sent = false; |
| 120 | - data.sent_time = new Date(); | |
| 121 | 159 | WP_Optimize_Heartbeat_Agents[agent_id] = data; |
| 160 | + | |
| 161 | + if ('undefined' !== typeof(data._wait) && false === data._wait) { | |
| 162 | + trigger_heartbeat(); | |
| 163 | + } | |
| 164 | + | |
| 122 | 165 | return agent_id; |
| 123 | 166 | } |
| 124 | 167 | |
| 125 | 168 | /** |
| 126 | - * Remove agent from list | |
| 169 | + * Trigger a heartbeat by code | |
| 127 | 170 | * |
| 171 | + * @returns {void} | |
| 172 | + */ | |
| 173 | + function trigger_heartbeat() { | |
| 174 | + if (is_heartbeat_api_disabled()) { | |
| 175 | + wpo_fallback.do_heartbeat(); | |
| 176 | + } else { | |
| 177 | + setTimeout(function() { wp.heartbeat.connectNow(); }, 50); | |
| 178 | + } | |
| 179 | + } | |
| 180 | + | |
| 181 | + /** | |
| 182 | + * Remove agent from list if `_keep:false`. Defaults to `true`, not cancel | |
| 183 | + * A method called cancel_agents that by default does not cancel anything is controversial, | |
| 184 | + * but in practice only things like informational requests can be really cancelled, | |
| 185 | + * otherwise you get strange inconsistent results when things get wiped out and callbacks are not being called. | |
| 186 | + * | |
| 128 | 187 | * @param {string} agent_id The id of the agent to be removed |
| 129 | 188 | * @returns {void} |
| 130 | 189 | */ |
| 131 | 190 | function cancel_agent(agent_id) { |
| 132 | - delete WP_Optimize_Heartbeat_Agents[agent_id]; | |
| 191 | + var agent = WP_Optimize_Heartbeat_Agents[agent_id]; | |
| 192 | + if('undefined' != typeof(agent)) { | |
| 193 | + if ('undefined' != typeof(agent._keep) && (false == agent._keep)) { | |
| 194 | + delete WP_Optimize_Heartbeat_Agents[agent_id]; | |
| 195 | + } | |
| 196 | + } | |
| 133 | 197 | } |
| 134 | 198 | |
| 135 | 199 | return { |
| 136 | 200 | setup: setup, |
| 137 | 201 | add_agent: add_agent, |
| 202 | + cancel_agents: cancel_agents, | |
| 138 | 203 | cancel_agent: cancel_agent |
| 139 | 204 | }; |
| 140 | 205 | } |
| 141 | 206 | |
| @@ -162,8 +227,10 @@ | ||
| 162 | 227 | * @param {int} interval How many seconds until next heartbeat |
| 163 | 228 | * @returns {void} |
| 164 | 229 | */ |
| 165 | 230 | function do_heartbeat(interval) { |
| 231 | + interval = 'undefined' == typeof(interval) ? payload.interval : interval; | |
| 232 | + | |
| 166 | 233 | var this_payload = Object.assign({}, payload); |
| 167 | 234 | var data = {}; |
| 168 | 235 | |
| 169 | 236 | jQuery(document).trigger('heartbeat-send', data); |
| @@ -181,9 +248,17 @@ | ||
| 181 | 248 | } |
| 182 | 249 | } |
| 183 | 250 | }); |
| 184 | 251 | |
| 252 | + if (timeout_handler) { | |
| 253 | + clearTimeout(timeout_handler); | |
| 254 | + } | |
| 255 | + | |
| 185 | 256 | timeout_handler = setTimeout(do_heartbeat, interval * 1000, interval); |
| 186 | 257 | } |
| 187 | 258 | |
| 188 | 259 | timeout_handler = setTimeout(do_heartbeat, payload.interval * 1000, payload.interval); |
| 260 | + | |
| 261 | + return { | |
| 262 | + do_heartbeat: do_heartbeat | |
| 263 | + }; | |
| 189 | 264 | } |