feat: Implement global request lifecycle cancellation on route transitions

Adds a navigation-aware request cancellation mechanism that automatically
aborts all route-specific API requests when the user navigates to a
different route. This prevents silent state-update errors from responses
arriving after component unmount and conserves bandwidth by cancelling
now-irrelevant requests.

Key additions:
- NavigationCancellationContext: Context for managing route-specific signals
- NavigationCancellationProvider: Provider that detects route changes and
  aborts all signals from the previous route
- useNavigationAbortSignal hook: Allows components to subscribe to
  navigation-aware cancellation signals
- Comprehensive tests for the cancellation lifecycle
- Documentation in Web-Development.md for request lifecycle policy

The provider is placed in the app hierarchy between BrowserRouter and
AuthProvider, ensuring consistent cancellation behavior across all routes.

Long-lived background tasks (polling, session validation) can opt-out by
managing their own AbortController lifecycle.

Closes #23 from Tasks.md: No global cancellation policy on route transitions

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-28 09:58:59 +02:00
parent e0a4d36fc3
commit 7ba1cf7ca2
8 changed files with 476 additions and 53 deletions

View File

@@ -2,13 +2,14 @@
* Application root component.
*
* Provider order (see `src/providers/PROVIDER_ORDER.md` for detailed contract):
* 1. `ThemeProvider` — OUTERMOST; provides theme context to AppContents
* 2. `FluentProvider` — supplies Fluent UI theme and design tokens
* 3. `NotificationProvider` — provides notification service to all descendants
* 4. `ErrorBoundary` — catches catastrophic errors
* 5. `BrowserRouter` — enables client-side routing via React Router
* 6. `AuthProvider` — manages session state; validates on mount; uses useNavigate()
* 7. `TimezoneProvider` — INNERMOST (inside protected routes); fetches timezone after auth
* 1. `ThemeProvider` — OUTERMOST; provides theme context to AppContents
* 2. `FluentProvider` — supplies Fluent UI theme and design tokens
* 3. `NotificationProvider` — provides notification service to all descendants
* 4. `ErrorBoundary` — catches catastrophic errors
* 5. `BrowserRouter` — enables client-side routing via React Router
* 6. `NavigationCancellationProvider` — manages route-aware request cancellation
* 7. `AuthProvider` — manages session state; validates on mount; uses useNavigate()
* 8. `TimezoneProvider` — INNERMOST (inside protected routes); fetches timezone after auth
*
* CRITICAL: Provider order is order-sensitive. See PROVIDER_ORDER.md before refactoring.
*
@@ -37,6 +38,7 @@ import { darkTheme, lightTheme } from "./theme/customTheme";
import { AuthProvider } from "./providers/AuthProvider";
import { ThemeProvider, useThemeMode } from "./providers/ThemeProvider";
import { TimezoneProvider } from "./providers/TimezoneProvider";
import { NavigationCancellationProvider } from "./providers/NavigationCancellationProvider";
import { NotificationProvider } from "./services/notificationService";
import { RequireAuth } from "./components/RequireAuth";
import { SetupGuard } from "./components/SetupGuard";
@@ -60,12 +62,13 @@ const BlocklistsPage = lazy(() => import("./pages/BlocklistsPage").then((m) => (
* Root application component — mounts providers and top-level routes.
*
* Provider stack (see PROVIDER_ORDER.md for detailed contract):
* - FluentProvider (2) — receives theme from useThemeMode()
* - NotificationProvider (3) — provides notification service
* - ErrorBoundary (4) — catches catastrophic errors
* - BrowserRouter (5) — enables routing
* - AuthProvider (6) — session validation; uses useNavigate()
* - TimezoneProvider (7) — inside protected routes only
* - FluentProvider (2) — receives theme from useThemeMode()
* - NotificationProvider (3) — provides notification service
* - ErrorBoundary (4) — catches catastrophic errors
* - BrowserRouter (5) — enables routing
* - NavigationCancellationProvider (6) — manages route-aware request cancellation
* - AuthProvider (7) — session validation; uses useNavigate()
* - TimezoneProvider (8) — inside protected routes only
*/
function AppContents(): React.JSX.Element {
const { colorMode } = useThemeMode();
@@ -89,9 +92,11 @@ function AppContents(): React.JSX.Element {
<NotificationContainer />
{/* 5. BrowserRouter — enables routing; required by AuthProvider's useNavigate() */}
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
<Suspense fallback={<Spinner size="large" label="Loading…" />}>
{/* 6. AuthProvider — validates session on mount; must be inside BrowserRouter */}
<AuthProvider>
{/* 6. NavigationCancellationProvider — manages route-aware request cancellation */}
<NavigationCancellationProvider>
<Suspense fallback={<Spinner size="large" label="Loading…" />}>
{/* 7. AuthProvider — validates session on mount; must be inside BrowserRouter */}
<AuthProvider>
<Routes>
{/* Setup wizard — always accessible; redirects to /login if already done */}
<Route
@@ -191,6 +196,7 @@ function AppContents(): React.JSX.Element {
</Routes>
</AuthProvider>
</Suspense>
</NavigationCancellationProvider>
</BrowserRouter>
</ErrorBoundary>
</NotificationProvider>