From d452c0081dcd4209bf79f71e5276fbbbc8c42ddc Mon Sep 17 00:00:00 2001 From: Chris Hill-Scott Date: Thu, 7 Jan 2021 11:18:19 +0000 Subject: [PATCH] Add throttling to AJAX calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The endpoint that count characters should be pretty low-load because it won’t talk to the database (unless, on the first request, the user and service aren’t cached in Redis). The response size is also very small, only one line of text wrapped in a single ``, so won’t be as CPU-intensive to render as a whole page. Still, we don’t want to completely hammer the server if a user types very quickly. This commit adds some throttling, so that we wait until there’s a certain amount of delay between keystrokes before firing off the request to the backend. I’ve set the delay at 150ms. At normal typing speed this makes the lag feel fairly imperceptible – it feels like you get an updated count in response to most keystrokes. It’s only if you really mash the keyboard that the count won’t update until you take a breath. --- app/assets/javascripts/updateStatus.js | 34 ++++++++++++++++++++++++-- tests/javascripts/updateStatus.test.js | 28 +++++++++++++++++++++ 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/updateStatus.js b/app/assets/javascripts/updateStatus.js index 071c622e0..a4a06b03b 100644 --- a/app/assets/javascripts/updateStatus.js +++ b/app/assets/javascripts/updateStatus.js @@ -3,10 +3,40 @@ window.GOVUK.Modules.UpdateStatus = function() { - let getRenderer = $component => response => $component.html( + const getRenderer = $component => response => $component.html( response.html ); + const throttle = (func, limit) => { + + let throttleOn = false; + let callsHaveBeenThrottled = false; + let timeout; + + return function() { + + const args = arguments; + const context = this; + + if (throttleOn) { + callsHaveBeenThrottled = true; + } else { + func.apply(context, args); + throttleOn = true; + } + + clearTimeout(timeout); + + timeout = setTimeout(() => { + throttleOn = false; + if (callsHaveBeenThrottled) func.apply(context, args); + callsHaveBeenThrottled = false; + }, limit); + + }; + + }; + this.start = component => { let id = 'update-status'; @@ -19,7 +49,7 @@ this.$textbox .attr('aria-described-by', this.$textbox.attr('aria-described-by') + ' ' + id) - .on('input', this.update) + .on('input', throttle(this.update, 150)) .trigger('input'); }; diff --git a/tests/javascripts/updateStatus.test.js b/tests/javascripts/updateStatus.test.js index 35fb360b1..ae0fa5b24 100644 --- a/tests/javascripts/updateStatus.test.js +++ b/tests/javascripts/updateStatus.test.js @@ -127,10 +127,38 @@ describe('Update content', () => { window.GOVUK.modules.start(); expect($.ajax.mock.calls.length).toEqual(1); + // 150ms of inactivity + jest.advanceTimersByTime(150); helpers.triggerEvent(textarea, 'input'); expect($.ajax.mock.calls.length).toEqual(2); }); + test("It should fire only after 150ms of inactivity", () => { + + let textarea = document.getElementById('template_content'); + + // Initial update triggered + window.GOVUK.modules.start(); + expect($.ajax.mock.calls.length).toEqual(1); + + helpers.triggerEvent(textarea, 'input'); + jest.advanceTimersByTime(149); + expect($.ajax.mock.calls.length).toEqual(1); + + helpers.triggerEvent(textarea, 'input'); + jest.advanceTimersByTime(149); + expect($.ajax.mock.calls.length).toEqual(1); + + helpers.triggerEvent(textarea, 'input'); + jest.advanceTimersByTime(149); + expect($.ajax.mock.calls.length).toEqual(1); + + // > 150ms of inactivity + jest.advanceTimersByTime(1); + expect($.ajax.mock.calls.length).toEqual(2); + + }); + });