Merge pull request #3405 from alphagov/ajax-backoff

Delay AJAX calls if the server is slow to respond
This commit is contained in:
Chris Hill-Scott
2020-04-09 12:19:32 +01:00
committed by GitHub
7 changed files with 52 additions and 37 deletions

View File

@@ -3,6 +3,12 @@
var queues = {}; var queues = {};
var dd = new global.diffDOM(); var dd = new global.diffDOM();
var interval = 0;
var calculateBackoff = responseTime => parseInt(Math.max(
(250 * Math.sqrt(responseTime)) - 1000,
1000
));
var getRenderer = $component => response => dd.apply( var getRenderer = $component => response => dd.apply(
$component.get(0), $component.get(0),
@@ -19,7 +25,9 @@
var clearQueue = queue => (queue.length = 0); var clearQueue = queue => (queue.length = 0);
var poll = function(renderer, resource, queue, interval, form) { var poll = function(renderer, resource, queue, form) {
let startTime = Date.now();
if (document.visibilityState !== "hidden" && queue.push(renderer) === 1) $.ajax( if (document.visibilityState !== "hidden" && queue.push(renderer) === 1) $.ajax(
resource, resource,
@@ -33,6 +41,7 @@
if (response.stop === 1) { if (response.stop === 1) {
poll = function(){}; poll = function(){};
} }
interval = calculateBackoff(Date.now() - startTime);
} }
).fail( ).fail(
() => poll = function(){} () => poll = function(){}
@@ -49,10 +58,11 @@
getRenderer($(component)), getRenderer($(component)),
$(component).data('resource'), $(component).data('resource'),
getQueue($(component).data('resource')), getQueue($(component).data('resource')),
($(component).data('interval-seconds') || 1.5) * 1000,
$(component).data('form') $(component).data('form')
); );
}; };
global.GOVUK.Modules.UpdateContent.calculateBackoff = calculateBackoff;
})(window); })(window);

View File

@@ -1,10 +1,9 @@
{% macro ajax_block(partials, url, key, interval=5, finished=False, form='') %} {% macro ajax_block(partials, url, key, finished=False, form='') %}
{% if not finished %} {% if not finished %}
<div <div
data-module="update-content" data-module="update-content"
data-resource="{{ url }}" data-resource="{{ url }}"
data-key="{{ key }}" data-key="{{ key }}"
data-interval-seconds="{{ interval }}"
data-form="{{ form }}" data-form="{{ form }}"
aria-live="polite" aria-live="polite"
> >

View File

@@ -19,25 +19,25 @@
{% include 'views/dashboard/write-first-messages.html' %} {% include 'views/dashboard/write-first-messages.html' %}
{% endif %} {% endif %}
{{ ajax_block(partials, updates_url, 'upcoming', interval=20) }} {{ ajax_block(partials, updates_url, 'upcoming') }}
<h2 class="heading-medium"> <h2 class="heading-medium">
In the last 7 days In the last 7 days
</h2> </h2>
{{ ajax_block(partials, updates_url, 'inbox', interval=20) }} {{ ajax_block(partials, updates_url, 'inbox') }}
{{ ajax_block(partials, updates_url, 'totals', interval=20) }} {{ ajax_block(partials, updates_url, 'totals') }}
{{ show_more( {{ show_more(
url_for('.monthly', service_id=current_service.id), url_for('.monthly', service_id=current_service.id),
'See messages sent per month' 'See messages sent per month'
) }} ) }}
{{ ajax_block(partials, updates_url, 'template-statistics', interval=20) }} {{ ajax_block(partials, updates_url, 'template-statistics') }}
{% if current_user.has_permissions('manage_service') %} {% if current_user.has_permissions('manage_service') %}
<h2 class='heading-medium'>This year</h2> <h2 class='heading-medium'>This year</h2>
{{ ajax_block(partials, updates_url, 'usage', interval=20) }} {{ ajax_block(partials, updates_url, 'usage') }}
{{ show_more( {{ show_more(
url_for(".usage", service_id=current_service['id']), url_for(".usage", service_id=current_service['id']),
'See usage' 'See usage'

View File

@@ -31,8 +31,7 @@
{{ ajax_block( {{ ajax_block(
partials, partials,
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status), url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status),
'counts', 'counts'
interval=20
) }} ) }}
{% call form_wrapper( {% call form_wrapper(
@@ -83,8 +82,7 @@
partials, partials,
url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page), url_for('.get_notifications_as_json', service_id=current_service.id, message_type=message_type, status=status, page=page),
'notifications', 'notifications',
form='search-form', form='search-form'
interval=20
) }} ) }}
{% endblock %} {% endblock %}

View File

@@ -96,10 +96,10 @@
</div> </div>
{% elif template.template_type == 'email' %} {% elif template.template_type == 'email' %}
<div class="js-stick-at-bottom-when-scrolling"> <div class="js-stick-at-bottom-when-scrolling">
{{ ajax_block(partials, updates_url, 'status', interval=2, finished=finished) }} {{ ajax_block(partials, updates_url, 'status', finished=finished) }}
</div> </div>
{% elif template.template_type == 'sms' %} {% elif template.template_type == 'sms' %}
{{ ajax_block(partials, updates_url, 'status', interval=2, finished=finished) }} {{ ajax_block(partials, updates_url, 'status', finished=finished) }}
{% endif %} {% endif %}
{% if current_user.has_permissions('send_messages') and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %} {% if current_user.has_permissions('send_messages') and current_user.has_permissions('view_activity') and template.template_type == 'sms' and can_receive_inbound %}

View File

@@ -50,6 +50,8 @@
"gulp-prettyerror": "1.2.1", "gulp-prettyerror": "1.2.1",
"gulp-sass-lint": "1.4.0", "gulp-sass-lint": "1.4.0",
"jest": "24.7.1", "jest": "24.7.1",
"jest-date-mock": "^1.0.8",
"jest-each": "^25.3.0",
"jshint": "2.10.2", "jshint": "2.10.2",
"jshint-stylish": "2.2.1", "jshint-stylish": "2.2.1",
"rollup-plugin-commonjs": "10.1.0", "rollup-plugin-commonjs": "10.1.0",

View File

@@ -1,3 +1,6 @@
const each = require('jest-each').default;
const jestDateMock = require('jest-date-mock');
const helpers = require('./support/helpers.js'); const helpers = require('./support/helpers.js');
const serviceNumber = '6658542f-0cad-491f-bec8-ab8457700ead'; const serviceNumber = '6658542f-0cad-491f-bec8-ab8457700ead';
@@ -18,6 +21,8 @@ beforeAll(() => {
// set up the object returned from $.ajax so it responds with whatever responseObj is set to // set up the object returned from $.ajax so it responds with whatever responseObj is set to
jqueryAJAXReturnObj = { jqueryAJAXReturnObj = {
done: callback => { done: callback => {
// The server takes 1 second to respond
jestDateMock.advanceBy(1000);
callback(responseObj); callback(responseObj);
return jqueryAJAXReturnObj; return jqueryAJAXReturnObj;
}, },
@@ -100,7 +105,7 @@ describe('Update content', () => {
jest.clearAllTimers(); jest.clearAllTimers();
}); });
test("It should make requests to the URL specified in the data-resource attribute", () => { test("It should make requests to the URL specified in the data-resource attribute", () => {
// start the module // start the module
@@ -136,24 +141,6 @@ describe('Update content', () => {
}); });
test("If an interval between requests is specified, using the data-interval-seconds attribute, requests should happen at that frequency", () => {
document.querySelector('[data-module=update-content]').setAttribute('data-interval-seconds', '0.5');
// start the module
window.GOVUK.modules.start();
expect($.ajax).toHaveBeenCalledTimes(1);
// units are milliseconds
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
jest.advanceTimersByTime(500);
expect($.ajax).toHaveBeenCalledTimes(4);
});
describe("By default", () => { describe("By default", () => {
beforeEach(() => { beforeEach(() => {
@@ -175,17 +162,36 @@ describe('Update content', () => {
}); });
test("It should request updates every 1.5 seconds", () => { test("It should request updates with a dynamic interval", () => {
// First call happens straight away
expect($.ajax).toHaveBeenCalledTimes(1); expect($.ajax).toHaveBeenCalledTimes(1);
// units are milliseconds // It took the server 1000ms to respond to the first call so we
jest.advanceTimersByTime(1500); // will back off the next call shouldnt happen in the next 6904ms
jest.advanceTimersByTime(6904);
expect($.ajax).toHaveBeenCalledTimes(1);
// But it should happen after 6905ms
jest.advanceTimersByTime(1);
expect($.ajax).toHaveBeenCalledTimes(2); expect($.ajax).toHaveBeenCalledTimes(2);
}); });
each([
[1000, 0],
[1500, 100],
[4590, 500],
[6905, 1000],
[24000, 10000],
]).test('It calculates a delay of %dms if the API responds in %dms', (waitTime, responseTime) => {
expect(
window.GOVUK.Modules.UpdateContent.calculateBackoff(responseTime)
).toBe(
waitTime
);
});
}); });
describe("If a form is used as a source for data, referenced in the data-form attribute", () => { describe("If a form is used as a source for data, referenced in the data-form attribute", () => {