Peter Sanchez: 1 Stop abusing tag autocomplete 3 files changed, 155 insertions(+), 74 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/183/mbox | git am -3Learn more about email & git
--- core/routes.go | 11 ++- static/js/advancedsearch.js | 133 +++++++++++++++++++++++------------- static/js/autocomplete.js | 85 ++++++++++++++++------- 3 files changed, 155 insertions(+), 74 deletions(-) diff --git a/core/routes.go b/core/routes.go index 5e8fd53..39a13f6 100644 --- a/core/routes.go +++ b/core/routes.go @@ -1,6 +1,7 @@ package core import ( + "context" "database/sql" "errors" "fmt" @@ -30,6 +31,7 @@ import ( "netlandish.com/x/gobwebs/database" "netlandish.com/x/gobwebs/messages" "netlandish.com/x/gobwebs/server" + "netlandish.com/x/gobwebs/timezone" "netlandish.com/x/gobwebs/validate" ) @@ -3447,7 +3449,14 @@ func (s *Service) TagAutocomplete(c echo.Context) error { } else { return c.JSON(http.StatusOK, "{}") } - tags, err = models.GetTags(c.Request().Context(), opts) + + // Create sql.DB context with timezone to avoid DBI connection hoarding + dbi := database.DBIForContext(c.Request().Context()) + tz := timezone.ForContext(c.Request().Context()) + newCtx := database.Context(context.Background(), dbi.GetDB()) + newCtx = timezone.Context(newCtx, tz) + + tags, err = models.GetTags(newCtx, opts) if err != nil { return err } diff --git a/static/js/advancedsearch.js b/static/js/advancedsearch.js index f800ce2..21ff3b6 100644 --- a/static/js/advancedsearch.js +++ b/static/js/advancedsearch.js @@ -8,6 +8,10 @@ var tagInputDiv = document.getElementById("advanced-search-div") // Track highlighted index for each autocomplete dropdown var highlightedIndexes = new WeakMap(); +// Track debounce timers and active requests for each tag selector +var debounceTimers = new WeakMap(); +var activeRequests = new WeakMap(); + // Track if we should allow form submission var allowFormSubmit = true; @@ -92,6 +96,19 @@ function autocomplete(event) { return; } + // Cancel previous request for this tag selector if active + var activeRequest = activeRequests.get(tagSelector); + if (activeRequest) { + activeRequest.abort(); + activeRequests.delete(tagSelector); + } + + // Clear existing debounce timer for this tag selector + var debounceTimer = debounceTimers.get(tagSelector); + if (debounceTimer) { + clearTimeout(debounceTimer); + } + tags = this.value.split(",") tag = tags[tags.length -1].trim() @@ -100,56 +117,78 @@ function autocomplete(event) { autocompleteTags = this.nextElementSibling; } if (tag !== "") { - // Store the query we're searching for - var searchQuery = tag; - - fetch(url + "&q=" + tag) - .then(function (response) { - if (!response.ok) { - console.log("Network response was not ok"); - } - return response.json(); - }) - .then(function (data) { - // Check if the current input still matches what we searched for - var currentTags = tagSelector.value.split(","); - var currentTag = currentTags[currentTags.length - 1].trim(); - - if (autocompleteTags !== undefined && currentTag === searchQuery) { - showSuggestions(autocompleteTags, data); + // Debounce the request for 300ms + var newTimer = setTimeout(function() { + // Store the query we're searching for + var searchQuery = tag; + + // Create AbortController for request cancellation + var controller = new AbortController(); + activeRequests.set(tagSelector, controller); + + fetch(url + "&q=" + tag, { signal: controller.signal }) + .then(function (response) { + if (!response.ok) { + console.log("Network response was not ok"); + } + return response.json(); + }) + .then(function (data) { + // Clear active request on completion + if (activeRequests.get(tagSelector) === controller) { + activeRequests.delete(tagSelector); + } - // Set up click handler for this dropdown - autocompleteTags.onclick = function(event) { - if (event.target.classList.contains("tag-suggested")) { - var tags = tagSelector.value.split(","); - var newTag = event.target.textContent + ", "; - if (tags.length > 1) { - newTag = " " + newTag; + // Check if the current input still matches what we searched for + var currentTags = tagSelector.value.split(","); + var currentTag = currentTags[currentTags.length - 1].trim(); + + if (autocompleteTags !== undefined && currentTag === searchQuery) { + showSuggestions(autocompleteTags, data); + + // Set up click handler for this dropdown + autocompleteTags.onclick = function(event) { + if (event.target.classList.contains("tag-suggested")) { + var tags = tagSelector.value.split(","); + var newTag = event.target.textContent + ", "; + if (tags.length > 1) { + newTag = " " + newTag; + } + tags[tags.length - 1] = newTag; + tagSelector.value = tags.join(","); + autocompleteTags.classList.add("d-none"); + highlightedIndexes.set(autocompleteTags, -1); + tagSelector.focus(); } - tags[tags.length - 1] = newTag; - tagSelector.value = tags.join(","); - autocompleteTags.classList.add("d-none"); - highlightedIndexes.set(autocompleteTags, -1); - tagSelector.focus(); - } - }; + }; + + // Set up mouseover handler for this dropdown + autocompleteTags.onmouseover = function(event) { + if (event.target.classList.contains("tag-suggested")) { + var index = parseInt(event.target.getAttribute("data-index")); + highlightSuggestion(autocompleteTags, index); + } + }; + } else if (autocompleteTags !== undefined) { + // Input has changed, hide the dropdown + autocompleteTags.classList.add("d-none"); + highlightedIndexes.set(autocompleteTags, -1); + } + }) + .catch(function (error) { + // Clear active request on error (unless aborted) + if (activeRequests.get(tagSelector) === controller) { + activeRequests.delete(tagSelector); + } - // Set up mouseover handler for this dropdown - autocompleteTags.onmouseover = function(event) { - if (event.target.classList.contains("tag-suggested")) { - var index = parseInt(event.target.getAttribute("data-index")); - highlightSuggestion(autocompleteTags, index); - } - }; - } else if (autocompleteTags !== undefined) { - // Input has changed, hide the dropdown - autocompleteTags.classList.add("d-none"); - highlightedIndexes.set(autocompleteTags, -1); - } - }) - .catch(function (error) { - console.log("Fetch error:", error); - }); + // Don't log aborted requests as errors + if (error.name !== 'AbortError') { + console.log("Fetch error:", error); + } + }); + }, 300); + + debounceTimers.set(tagSelector, newTimer); } else { if (autocompleteTags !== undefined) { autocompleteTags.classList.add("d-none"); diff --git a/static/js/autocomplete.js b/static/js/autocomplete.js index 5593795..30b079a 100644 --- a/static/js/autocomplete.js +++ b/static/js/autocomplete.js @@ -3,6 +3,8 @@ var tagSelector = document.getElementById("tag-selector"); var autocompleteTags = document.getElementById("autocomplete-tags"); var highlightedIndex = -1; var suggestions = []; +var debounceTimer; +var activeRequest; // Function to highlight a suggestion function highlightSuggestion(index) { @@ -111,36 +113,67 @@ tagSelector.addEventListener("keyup", function(event) { return; } + // Cancel previous request if active + if (activeRequest) { + activeRequest.abort(); + activeRequest = null; + } + + // Clear existing debounce timer + if (debounceTimer) { + clearTimeout(debounceTimer); + } + tags = this.value.split(","); tag = tags[tags.length - 1].trim(); if (tag !== "") { - // Store the query we're searching for - var searchQuery = tag; - - fetch(url + "&q=" + tag) - .then(function (response) { - if (!response.ok) { - console.log("Network response was not ok"); - } - return response.json(); - }) - .then(function (data) { - // Check if the current input still matches what we searched for - var currentTags = tagSelector.value.split(","); - var currentTag = currentTags[currentTags.length - 1].trim(); - - if (currentTag === searchQuery) { - showSuggestions(data); - } else { - // Input has changed, hide the dropdown - autocompleteTags.classList.add("d-none"); - highlightedIndex = -1; - } - }) - .catch(function (error) { - console.log("Fetch error:", error); - }); + // Debounce the request for 300ms + debounceTimer = setTimeout(function() { + // Store the query we're searching for + var searchQuery = tag; + + // Create AbortController for request cancellation + var controller = new AbortController(); + activeRequest = controller; + + fetch(url + "&q=" + tag, { signal: controller.signal }) + .then(function (response) { + if (!response.ok) { + console.log("Network response was not ok"); + } + return response.json(); + }) + .then(function (data) { + // Clear active request on completion + if (activeRequest === controller) { + activeRequest = null; + } + + // Check if the current input still matches what we searched for + var currentTags = tagSelector.value.split(","); + var currentTag = currentTags[currentTags.length - 1].trim(); + + if (currentTag === searchQuery) { + showSuggestions(data); + } else { + // Input has changed, hide the dropdown + autocompleteTags.classList.add("d-none"); + highlightedIndex = -1; + } + }) + .catch(function (error) { + // Clear active request on error (unless aborted) + if (activeRequest === controller) { + activeRequest = null; + } + + // Don't log aborted requests as errors + if (error.name !== 'AbortError') { + console.log("Fetch error:", error); + } + }); + }, 300); } else { autocompleteTags.classList.add("d-none"); highlightedIndex = -1; -- 2.49.1
Applied. To git@git.code.netlandish.com:~netlandish/links 4f1684a..5f89d68 master -> master