951a522
import { createAlova } from 'alova';
import fetchAdapter from 'alova/fetch';
import { createApis, withConfigType, mountApis } from './createApis';
import { recordApiResult } from './health';
import { newTraceparent } from './trace';
const rawAdapter = fetchAdapter();
let refreshInFlight: Promise<boolean> | null = null;
function refreshSession(): Promise<boolean> {
if (!refreshInFlight) {
refreshInFlight = fetch('/api/v1/auth/refresh', {
method: 'POST',
credentials: 'same-origin'
})
.then(res => res.ok)
.catch(() => false)
.finally(() => {
refreshInFlight = null;
});
}
return refreshInFlight;
}
function redirectToLogin() {
const { pathname, search } = window.location;
if (pathname.startsWith('/auth')) return;
// 带着当前位置重登,回来后还停在原来的页面;入口页不重复带参数。
const target = pathname === '/' || pathname === '/app' ? '' : `&redirect=${encodeURIComponent(pathname + search)}`;
window.location.assign(`/api/v1/auth/login?silent=1${target}`);
}
const retryAdapter: typeof rawAdapter = (elements, method) => {
const original = rawAdapter(elements, method);
return {
abort: original.abort,
headers: original.headers,
onDownload: original.onDownload,
onUpload: original.onUpload,
response: async () => {
const response = await original.response();
if (response.status !== 401) return response;
const body = await response.clone().text();
if (!body.includes('token_expired')) return response;
if (!(await refreshSession())) {
redirectToLogin();
return response;
}
return rawAdapter(elements, method).response();
}
};
};
export const alovaInstance = createAlova({
baseURL: '/api/v1',
requestAdapter: retryAdapter,
beforeRequest: method => {
method.config.headers = {
...method.config.headers,
traceparent: newTraceparent()
};
},
responded: {
// Every settled response is reported to the health window before its body
// is parsed, so an unreachable backend is recognised even when the caller
// swallows the resulting error.
onSuccess: async (res, method) => {
recordApiResult({ status: res.status, url: method.url });
const contentType = res.headers.get('content-type') || '';
let body: unknown;
if (contentType.includes('application/json')) {
try {
body = await res.json();
} catch (err) {
// A gateway error page is handled by the envelope fallback below; a
// broken success payload must still reject the method unchanged.
if (res.status < 400) throw err;
body = null;
}
} else {
body = await res.text();
}
// A failing hop (dev-server proxy, reverse proxy, gateway) answers with
// plain text, never with the documented envelope. Synthesise one so
// `unwrapApi` still throws an ApiFailure that carries the real status
// instead of `code: undefined`.
if (res.status >= 400 && !isEnvelope(body)) {
return {
code: res.status,
message: res.status >= 500 ? '服务未响应(网关错误)' : '服务返回了非接口数据',
data: null,
error: null,
error_code: null
};
}
return body;
},
// Alova routes only real transport failures here (404/500 land on
// onSuccess). The error must be rethrown: a global onError that returns
// normally makes alova resolve the method instead, so callers would read
// `.code` off `undefined` and lose their ApiFailure/network-error branch.
onError: (err, method) => {
if ((err as Error | null)?.name !== 'AbortError') {
recordApiResult({ status: 0, url: method.url, networkError: true });
}
throw err;
}
}
});
function isEnvelope(value: unknown): value is { code: number } {
return typeof value === 'object' && value !== null && typeof (value as { code?: unknown }).code === 'number';
}
export const $$userConfigMap = withConfigType({});
const Apis = createApis(alovaInstance, $$userConfigMap);
mountApis(Apis);
export default Apis;
// Health gate primitives live next to the transport they observe; exported
// through the segment's public API so `app` and `entities` never deep-import.
export {
API_ERROR_BURST_FAILURES,
API_ERROR_MIN_SAMPLES,
API_ERROR_RATE_THRESHOLD,
API_PROBE_INTERVAL_MS,
getApiHealth,
recordApiResult,
retryApiHealth,
subscribeApiHealth,
trackFetch,
useApiHealth,
useApiUnavailable,
type ApiHealth,
type ApiResult
} from './health';
export { newTraceparent } from './trace';
// Re-export the generated DTO types through the segment's public API so
// consumers never deep-import globals.d (regenerated file, see alova.config).
export type * from './globals.d'