fix(ui): serialize active-server activations to guarantee ordering

Aborting a fetch can't stop a POST already running server-side, so a stale
activation could still land last. Chain the requests so each runs after the
previous resolves — the latest navigation's activation is always last. A 5s
per-request timeout keeps a hung request from stalling the chain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-14 19:34:19 +02:00
parent f4c24d51a6
commit a69551a7eb
1 changed files with 16 additions and 11 deletions

View File

@ -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=<activate URL>.
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, {
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: clusevActivateAbort.signal,
}).catch(() => {});
headers: { 'X-CSRF-TOKEN': token, 'X-Requested-With': 'XMLHttpRequest' },
signal: ctrl.signal,
}).catch(() => {}).finally(() => clearTimeout(timer));
});
});