Implement request deduplication to prevent multiple duplicate calls to GET /api/setup when multiple components mount simultaneously. The fix introduces: 1. New 'useSharedSetupStatus' hook with module-level caching - Shares a single in-flight request across all consumers - Implements 30-second cache TTL with cache invalidation - Notifies all subscribers when cache is invalidated 2. Refactored 'useSetup' hook to use shared cache - Internally uses useSharedSetupStatus for status checks - Calls invalidateSetupStatus() after successful setup submission - Maintains backward-compatible API 3. Updated components using setup status - SetupGuard and SetupPage automatically benefit from deduplication - No changes needed to consumer code 4. Updated tests - Mocked useSharedSetupStatus in component tests - Added comprehensive tests for cache behavior - All existing tests pass 5. Documentation updates - Added 'Request Deduplication & Shared Caching' section to Web-Development.md - Explains when and how to use shared hooks - Provides complete implementation example This eliminates wasted resources from duplicate API calls and potential race conditions where different requests return slightly different states. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
72 lines
2.1 KiB
TypeScript
72 lines
2.1 KiB
TypeScript
/**
|
|
* Hook for the initial BanGUI setup flow.
|
|
*
|
|
* Exposes the current setup completion status and a submission handler.
|
|
* Uses the shared setup status hook to deduplicate requests when multiple
|
|
* consumers mount simultaneously.
|
|
*/
|
|
|
|
import { useCallback, useState } from "react";
|
|
import { ApiError } from "../api/client";
|
|
import { submitSetup } from "../api/setup";
|
|
import { invalidateSetupStatus, useSharedSetupStatus } from "./useSharedSetupStatus";
|
|
import type {
|
|
SetupRequest,
|
|
SetupStatusResponse,
|
|
} from "../types/setup";
|
|
|
|
export interface UseSetupResult {
|
|
/** Known setup status, or null while loading. */
|
|
status: SetupStatusResponse | null;
|
|
/** Whether the initial status check is in progress. */
|
|
loading: boolean;
|
|
/** User-facing error message from the last status check. */
|
|
error: string | null;
|
|
/** Refresh the setup status from the backend. */
|
|
refresh: () => Promise<void>;
|
|
/** Whether a submit request is currently in flight. */
|
|
submitting: boolean;
|
|
/** User-facing error message from the last submit attempt. */
|
|
submitError: string | null;
|
|
/** Submit the initial setup payload. */
|
|
submit: (payload: SetupRequest) => Promise<void>;
|
|
}
|
|
|
|
export function useSetup(): UseSetupResult {
|
|
const { status, loading, error, refresh } = useSharedSetupStatus();
|
|
const [submitting, setSubmitting] = useState(false);
|
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
|
|
|
const submit = useCallback(async (payload: SetupRequest): Promise<void> => {
|
|
setSubmitting(true);
|
|
setSubmitError(null);
|
|
|
|
try {
|
|
await submitSetup(payload);
|
|
// Invalidate the cache after successful setup so the next check reflects the new state.
|
|
invalidateSetupStatus();
|
|
} catch (err: unknown) {
|
|
if (err instanceof ApiError) {
|
|
setSubmitError(err.message);
|
|
} else if (err instanceof Error) {
|
|
setSubmitError(err.message);
|
|
} else {
|
|
setSubmitError("An unexpected error occurred.");
|
|
}
|
|
throw err;
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
}, []);
|
|
|
|
return {
|
|
status,
|
|
loading,
|
|
error,
|
|
refresh,
|
|
submitting,
|
|
submitError,
|
|
submit,
|
|
};
|
|
}
|