diff --git a/resources/js/app.js b/resources/js/app.js index fa8a6ae..5d2188d 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -202,18 +202,23 @@ document.addEventListener('alpine:init', () => { // Keep the active-server session in sync with the server-details page on screen, even // when wire:navigate restores a page from its bfcache (Back/Forward) without re-running // Show::mount(). The details page tags its root with data-clusev-activate=. -let clusevActivateAbort; +let clusevActivateChain = Promise.resolve(); document.addEventListener('livewire:navigated', () => { - // Abort any prior in-flight activation so only the LATEST navigation can win — a - // stale request must never overwrite a newer server selection. - clusevActivateAbort?.abort(); const el = document.querySelector('[data-clusev-activate]'); if (!el) return; - clusevActivateAbort = new AbortController(); - const token = document.querySelector('meta[name="csrf-token"]')?.content; - fetch(el.dataset.clusevActivate, { - method: 'POST', - headers: { 'X-CSRF-TOKEN': token ?? '', 'X-Requested-With': 'XMLHttpRequest' }, - signal: clusevActivateAbort.signal, - }).catch(() => {}); + const url = el.dataset.clusevActivate; + const token = document.querySelector('meta[name="csrf-token"]')?.content ?? ''; + // SERIALIZE: run each activation after the previous one resolves, so the LATEST + // navigation's request is guaranteed to reach the server last — a stale activation + // can never overwrite a newer selection. A per-request timeout prevents a hung + // request from stalling the chain. + clusevActivateChain = clusevActivateChain.then(() => { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 5000); + return fetch(url, { + method: 'POST', + headers: { 'X-CSRF-TOKEN': token, 'X-Requested-With': 'XMLHttpRequest' }, + signal: ctrl.signal, + }).catch(() => {}).finally(() => clearTimeout(timer)); + }); });