Files
PolyWeather/frontend/components/dashboard/scan-terminal/use-remote-data-query.ts
T
2569718930@qq.com b122e7cbae Stabilize the decision workspace data boundaries
The scan terminal had grown into overlapping CSS, request-state, AI-provider, and city-card data responsibilities. This refactor separates those boundaries without changing product behavior: CSS modules are split by surface, city AI prompt/provider/fallback logic is isolated, and scan terminal request state now has reusable RemoteData adapters plus business-state tests.

Constraint: Preserve existing global scan-terminal class names and API responses during the refactor

Constraint: No new dependencies; keep this as a file-boundary cleanup

Rejected: Introduce React Query now | higher migration risk than the requested lightweight query-client path

Rejected: Rewrite AI stream behavior | progressive/fallback states are product-sensitive and were only adapter-split

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Keep AI stream state changes covered by business snapshots before changing fallback/cache wording

Tested: npm run test:business; npx tsc --noEmit; npm run build; python pytest -q; ruff check; py_compile targeted city AI modules

Not-tested: Live DeepSeek provider network replay and browser visual QA
2026-04-28 14:45:34 +08:00

115 lines
2.9 KiB
TypeScript

"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import {
toRemoteError,
toRemoteLoading,
toRemoteSuccess,
type RemoteData,
} from "@/components/dashboard/scan-terminal/scan-terminal-client";
type RunRemoteQueryOptions<T> = {
onSuccess?: (data: T) => void;
request: (signal: AbortSignal) => Promise<T>;
showLoading?: boolean;
};
export function useRemoteDataQuery<T>() {
const [data, setData] = useState<T | null>(null);
const [remote, setRemote] = useState<RemoteData<T>>({ status: "idle" });
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const abortRef = useRef<AbortController | null>(null);
const requestSeqRef = useRef(0);
const loadingRef = useRef(false);
const abort = useCallback(() => {
abortRef.current?.abort();
abortRef.current = null;
}, []);
const reset = useCallback(() => {
requestSeqRef.current += 1;
abort();
loadingRef.current = false;
setLoading(false);
setError(null);
setData(null);
setRemote({ status: "idle" });
}, [abort]);
const setSuccess = useCallback(
(nextData: T) => {
requestSeqRef.current += 1;
abort();
loadingRef.current = false;
setLoading(false);
setError(null);
setData(nextData);
setRemote(toRemoteSuccess(nextData));
},
[abort],
);
const run = useCallback(
async ({
onSuccess,
request,
showLoading = false,
}: RunRemoteQueryOptions<T>) => {
const requestSeq = ++requestSeqRef.current;
abortRef.current?.abort();
const controller = new AbortController();
abortRef.current = controller;
if (showLoading) {
loadingRef.current = true;
setLoading(true);
setRemote((current) => toRemoteLoading(current));
}
setError(null);
try {
const payload = await request(controller.signal);
if (requestSeq !== requestSeqRef.current) return null;
setData(payload);
setRemote(toRemoteSuccess(payload));
setError(null);
onSuccess?.(payload);
return payload;
} catch (caught) {
if (controller.signal.aborted || requestSeq !== requestSeqRef.current) {
return null;
}
const message = caught instanceof Error ? caught.message : String(caught);
setError(message);
setRemote((current) => toRemoteError(caught, current));
return null;
} finally {
if (abortRef.current === controller) {
abortRef.current = null;
}
if (showLoading) {
loadingRef.current = false;
setLoading(false);
}
}
},
[],
);
const isLoading = useCallback(() => loadingRef.current, []);
useEffect(() => abort, [abort]);
return {
abort,
data,
error,
isLoading,
loading,
remote,
reset,
run,
setSuccess,
};
}