fix: prevent race conditions in htmx boost response handling

This commit is contained in:
Björn Fromme
2026-03-16 12:03:00 +01:00
parent 29a1ce7de7
commit cf95048f2b
+27 -5
View File
@@ -6,6 +6,7 @@ const DEBOUNCE_MS = 200
let debounceTimeout = null let debounceTimeout = null
let historyRestoreGracePeriod = false let historyRestoreGracePeriod = false
let savedScrollPosition = null let savedScrollPosition = null
let activeRequests = 0
function getIndicator() { function getIndicator() {
return document.getElementById(INDICATOR_ID) return document.getElementById(INDICATOR_ID)
@@ -35,6 +36,8 @@ function handleBeforeRequest(event) {
return return
} }
activeRequests++
// Save scroll position of the target container before HTMX request // Save scroll position of the target container before HTMX request
const target = event.detail?.target || document.getElementById('main-content') const target = event.detail?.target || document.getElementById('main-content')
if (target && typeof target.scrollTop === 'number') { if (target && typeof target.scrollTop === 'number') {
@@ -52,6 +55,19 @@ function handleBeforeRequest(event) {
}, DEBOUNCE_MS) }, DEBOUNCE_MS)
} }
function handleRequestEnd() {
// Counter may already be 0 if a force-reset (history save, page show) fired
// before this afterRequest — ignore the stale completion in that case
if (0 === activeRequests) {
return
}
activeRequests--
if (0 === activeRequests) {
hide()
}
}
function handleAfterSwap() { function handleAfterSwap() {
// Restore scroll position after HTMX swaps content // Restore scroll position after HTMX swaps content
if (savedScrollPosition !== null && savedScrollPosition.element) { if (savedScrollPosition !== null && savedScrollPosition.element) {
@@ -61,6 +77,7 @@ function handleAfterSwap() {
} }
function handleHistoryRestore() { function handleHistoryRestore() {
activeRequests = 0
hide() hide()
// Set grace period to ignore change events triggered by history restore // Set grace period to ignore change events triggered by history restore
@@ -71,24 +88,29 @@ function handleHistoryRestore() {
} }
function handleTimeout() { function handleTimeout() {
hide() // Don't decrement here — htmx:afterRequest always fires alongside htmx:timeout
// and handles the counter. This handler only shows the alert.
alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.') alert('Die Anfrage hat zu lange gedauert. Bitte versuchen Sie es erneut. Falls das Problem weiterhin besteht, kontaktieren Sie bitte unseren Support.')
} }
function handleBeforeHistorySave() { function handleBeforeHistorySave() {
// Hide indicator before HTMX saves the page to history cache // Hide indicator before HTMX saves the page to history cache
activeRequests = 0
hide()
}
function handlePageShow() {
activeRequests = 0
hide() hide()
} }
// Initialize event listeners // Initialize event listeners
document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest) document.body.addEventListener('htmx:beforeRequest', handleBeforeRequest)
document.body.addEventListener('htmx:afterRequest', hide) document.body.addEventListener('htmx:afterRequest', handleRequestEnd)
document.body.addEventListener('htmx:afterSwap', handleAfterSwap) document.body.addEventListener('htmx:afterSwap', handleAfterSwap)
document.body.addEventListener('htmx:timeout', handleTimeout) document.body.addEventListener('htmx:timeout', handleTimeout)
document.body.addEventListener('htmx:historyRestore', handleHistoryRestore) document.body.addEventListener('htmx:historyRestore', handleHistoryRestore)
document.body.addEventListener('htmx:sendError', hide)
document.body.addEventListener('htmx:responseError', hide)
document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave) document.body.addEventListener('htmx:beforeHistorySave', handleBeforeHistorySave)
window.addEventListener('pageshow', hide) window.addEventListener('pageshow', handlePageShow)
window.addEventListener('popstate', handleHistoryRestore) window.addEventListener('popstate', handleHistoryRestore)