اپ تا الان هیچ درخواستی به سرور نمیزد و همهی دادهها داخلِ خودِ کامپوننتها
هاردکد بود. این کامیت لایهی ارتباط را اضافه میکند، بدونِ اینکه هیچ صفحهای
را تغییر دهد.
عمداً فقط فایلِ جدید اضافه شده و App.tsx دست نخورده است، چون کارِ در جریانِ
برنچِ updateApp روی همان فایل است و نباید با هم تصادف کنند. وصل کردنِ
صفحهها به این لایه کارِ بعدیست.
هیچ پکیجِ جدیدی اضافه نشد — فقط fetch استاندارد.
چیزهایی که در client.ts هست برای حالتِ خراباند، نه حالتِ خوب:
• timeout روی هر درخواست — وگرنه بکاندِ کند سوکتها را باز نگه میدارد تا
مرورگر به سقفِ ۶ درخواستِ همزمان بخورد و اپ قفل شود.
• backoff با jitter — بدونِ jitter همهی کلاینتها بعد از یک خطا همزمان
دوباره میزنند و موجِ دوم از موجِ اول بدتر است.
• تکرارِ خودکار فقط روی GET؛ POST فقط وقتی Idempotency-Key دارد (ثبتِ پاسخ،
سفارش) تا دابلکلیکِ کاربر دو بار XP یا دو سفارش نسازد.
• refresh تکنسخهای — ۲۰ درخواستِ همزمانِ ۴۰۱ نباید ۲۰ بار refresh بزنند.
• ادغامِ GETهای یکسانِ در جریان.
• احترام به هدرِ Retry-After که بکاند موقعِ throttle میفرستد.
credentials: 'omit' عمدیست: احرازِ هویت توکنِ Bearer در هدر است و سمتِ سرور
CORS_ALLOW_CREDENTIALS=False تنظیم شده.
.env هم به .gitignore اضافه شد؛ قبلاً فقط node_modules داخلش بود و آدرس/
تنظیماتِ محلی میتوانست ناخواسته کامیت شود.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
3.4 KiB
TypeScript
88 lines
3.4 KiB
TypeScript
/**
|
||
* بررسیِ اتصال به بکاند — ابزارِ توسعه، نه بخشی از محصول.
|
||
*
|
||
* برای امتحان کردن، یک خط در App.tsx (یا هر جای دیگر) کافیست:
|
||
*
|
||
* import { ConnectionCheck } from '@/api/ConnectionCheck';
|
||
* <ConnectionCheck />
|
||
*
|
||
* هیچ فایلِ دیگری را عوض نمیکند و اگر بکاند بالا نباشد هم اپ را نمیخواباند.
|
||
*/
|
||
import { useEffect, useState } from 'react';
|
||
import type { CSSProperties } from 'react';
|
||
import { api, ApiError } from '@/api';
|
||
import type { Grade } from '@/api';
|
||
|
||
type State =
|
||
| { kind: 'loading' }
|
||
| { kind: 'ok'; grades: Grade[] }
|
||
| { kind: 'error'; message: string; hint: string };
|
||
|
||
function hintFor(error: unknown): string {
|
||
if (!(error instanceof ApiError)) return 'خطای پیشبینینشده — کنسول را ببینید.';
|
||
if (error.isNetwork) {
|
||
return 'سرور جواب نداد. VITE_API_BASE_URL درست است؟ بکاند بالاست؟';
|
||
}
|
||
if (error.status === 502) {
|
||
return 'nginx بالاست ولی جنگو نه — سرویسِ بکاند روی سرور اجرا نشده.';
|
||
}
|
||
if (error.status === 0) return 'ارتباط برقرار نشد.';
|
||
if (error.isRateLimited) return 'سقفِ نرخ خورد؛ کلاینت خودش دوباره تلاش میکند.';
|
||
if (error.isUnauthorized) return 'نیاز به ورود دارد (این اندپوینت نباید داشته باشد).';
|
||
return `سرور ${error.status} داد.`;
|
||
}
|
||
|
||
export function ConnectionCheck() {
|
||
const [state, setState] = useState<State>({ kind: 'loading' });
|
||
|
||
useEffect(() => {
|
||
const controller = new AbortController();
|
||
api
|
||
.grades({ signal: controller.signal })
|
||
.then((grades) => setState({ kind: 'ok', grades }))
|
||
.catch((error: unknown) => {
|
||
if (controller.signal.aborted) return;
|
||
setState({
|
||
kind: 'error',
|
||
message: error instanceof Error ? error.message : String(error),
|
||
hint: hintFor(error),
|
||
});
|
||
});
|
||
return () => controller.abort();
|
||
}, []);
|
||
|
||
const box: CSSProperties = {
|
||
position: 'fixed', bottom: 12, right: 12, zIndex: 9999,
|
||
maxWidth: 320, padding: '10px 12px', borderRadius: 10,
|
||
font: '12px/1.6 system-ui, sans-serif', direction: 'rtl',
|
||
background: '#fff', border: '1px solid #e5e7eb',
|
||
boxShadow: '0 4px 16px rgba(0,0,0,.08)',
|
||
};
|
||
|
||
if (state.kind === 'loading') {
|
||
return <div style={box}>در حالِ بررسیِ اتصال به بکاند…</div>;
|
||
}
|
||
|
||
if (state.kind === 'error') {
|
||
return (
|
||
<div style={{ ...box, borderColor: '#fecaca' }}>
|
||
<b style={{ color: '#b91c1c' }}>اتصال برقرار نشد</b>
|
||
<div style={{ marginTop: 4 }}>{state.message}</div>
|
||
<div style={{ marginTop: 4, color: '#6b7280' }}>{state.hint}</div>
|
||
<div style={{ marginTop: 6, color: '#9ca3af' }}>{api.http.baseUrl}</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div style={{ ...box, borderColor: '#bbf7d0' }}>
|
||
<b style={{ color: '#15803d' }}>اتصال برقرار است</b>
|
||
<div style={{ marginTop: 4 }}>
|
||
{state.grades.length} پایه از سرور آمد
|
||
{state.grades.length > 0 && `: ${state.grades.map((g) => g.title).join('، ')}`}
|
||
</div>
|
||
<div style={{ marginTop: 6, color: '#9ca3af' }}>{api.http.baseUrl}</div>
|
||
</div>
|
||
);
|
||
}
|