forked from Tadrisino/MathApp_frontend
add a typed API layer for the Tadrisino backend
اپ تا الان هیچ درخواستی به سرور نمیزد و همهی دادهها داخلِ خودِ کامپوننتها
هاردکد بود. این کامیت لایهی ارتباط را اضافه میکند، بدونِ اینکه هیچ صفحهای
را تغییر دهد.
عمداً فقط فایلِ جدید اضافه شده و 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>
This commit is contained in:
87
src/api/ConnectionCheck.tsx
Normal file
87
src/api/ConnectionCheck.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* بررسیِ اتصال به بکاند — ابزارِ توسعه، نه بخشی از محصول.
|
||||
*
|
||||
* برای امتحان کردن، یک خط در 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user