---
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