fix and update
This commit is contained in:
@@ -40,6 +40,8 @@ interface AuthContextValue {
|
||||
token: string | null;
|
||||
isLoading: boolean;
|
||||
isApiReady: boolean;
|
||||
/** false первые ~600 ms после bootstrap — блокирует шторм API сразу после логина */
|
||||
isContentReady: boolean;
|
||||
isPinLocked: boolean;
|
||||
hasStoredSession: boolean;
|
||||
login: (login: string, password: string) => Promise<AuthTokens>;
|
||||
@@ -109,6 +111,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
const [user, setUser] = React.useState<PublicUser | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(true);
|
||||
const [isApiReady, setIsApiReady] = React.useState(false);
|
||||
const [isContentReady, setIsContentReady] = React.useState(false);
|
||||
const [isPinLocked, setIsPinLocked] = React.useState(false);
|
||||
const [hasStoredSession, setHasStoredSession] = React.useState(false);
|
||||
const [lockedSessionId, setLockedSessionId] = React.useState<string | null>(null);
|
||||
@@ -177,8 +180,9 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
initialBootstrapDoneRef.current = true;
|
||||
setIsApiReady(true);
|
||||
setIsLoading(false);
|
||||
setIsContentReady(false);
|
||||
if (auth.pinVerified !== false) {
|
||||
scheduleFedcmSessionSync(auth.accessToken, 2000);
|
||||
scheduleFedcmSessionSync(auth.accessToken, 2500);
|
||||
}
|
||||
},
|
||||
[clearPinLock]
|
||||
@@ -546,6 +550,21 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
Boolean(hasStoredSession || token || user) &&
|
||||
(isLoading || !isApiReady);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (isBootstrapping) {
|
||||
setIsContentReady(false);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const timer = window.setTimeout(() => {
|
||||
if (!cancelled) setIsContentReady(true);
|
||||
}, 650);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [isBootstrapping]);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
@@ -553,6 +572,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
token,
|
||||
isLoading,
|
||||
isApiReady,
|
||||
isContentReady,
|
||||
isPinLocked,
|
||||
hasStoredSession,
|
||||
login,
|
||||
@@ -570,7 +590,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
||||
logout
|
||||
}}
|
||||
>
|
||||
{isBootstrapping ? <AppBootstrapScreen /> : children}
|
||||
{isBootstrapping || !isContentReady ? <AppBootstrapScreen /> : children}
|
||||
<PinLockModal open={isPinLocked} isSubmitting={pinSubmitting} error={pinError} onSubmit={handlePinUnlock} />
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,6 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover
|
||||
import {
|
||||
AppNotification,
|
||||
fetchNotifications,
|
||||
fetchUnreadNotificationCount,
|
||||
getApiErrorMessage,
|
||||
markAllNotificationsRead,
|
||||
markNotificationRead,
|
||||
@@ -27,7 +26,7 @@ function formatTime(value: string) {
|
||||
|
||||
export function NotificationBell() {
|
||||
const router = useRouter();
|
||||
const { token, isPinLocked, isLoading, isApiReady } = useAuth();
|
||||
const { token, isPinLocked, isLoading, isContentReady } = useAuth();
|
||||
const { showToast } = useToast();
|
||||
const { unreadCount, setUnreadCount, subscribe } = useRealtime();
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -36,23 +35,23 @@ export function NotificationBell() {
|
||||
const [respondingId, setRespondingId] = useState<string | null>(null);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
if (!token || isPinLocked || isLoading || !isApiReady) return;
|
||||
if (!token || isPinLocked || isLoading || !isContentReady) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
const [list, count] = await Promise.all([fetchNotifications(token), fetchUnreadNotificationCount(token)]);
|
||||
const list = await fetchNotifications(token);
|
||||
setItems(list.notifications ?? []);
|
||||
setUnreadCount(count.count ?? 0);
|
||||
} catch (error) {
|
||||
const message = getApiErrorMessage(error, 'Не удалось загрузить уведомления');
|
||||
if (message) showToast(message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [isApiReady, isLoading, isPinLocked, setUnreadCount, showToast, token]);
|
||||
}, [isContentReady, isLoading, isPinLocked, showToast, token]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
void load();
|
||||
}, [load]);
|
||||
}, [load, open]);
|
||||
|
||||
useEffect(() => {
|
||||
return subscribe((event) => {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import * as React from 'react';
|
||||
import { useAuth } from '@/components/id/auth-provider';
|
||||
import { ensureApiGatewayReady, getWsUrl, type ChatMessage } from '@/lib/api';
|
||||
import { ensureApiGatewayReady, fetchUnreadNotificationCount, getWsUrl, type ChatMessage } from '@/lib/api';
|
||||
import { dispatchAvatarUpdated, dispatchChatRoomAvatarUpdated } from '@/lib/avatar-events';
|
||||
|
||||
export interface RealtimeEvent {
|
||||
@@ -31,7 +31,7 @@ interface RealtimeContextValue {
|
||||
const RealtimeContext = React.createContext<RealtimeContextValue | null>(null);
|
||||
|
||||
export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
||||
const { user, token, isPinLocked, isLoading, isApiReady } = useAuth();
|
||||
const { user, token, isPinLocked, isLoading, isContentReady } = useAuth();
|
||||
const [unreadCount, setUnreadCount] = React.useState(0);
|
||||
const [connected, setConnected] = React.useState(false);
|
||||
const listenersRef = React.useRef(new Set<Listener>());
|
||||
@@ -48,7 +48,26 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!user || !token || isPinLocked || isLoading || !isApiReady) {
|
||||
if (!user || !token || isPinLocked || isLoading || !isContentReady) {
|
||||
setUnreadCount(0);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
const timer = window.setTimeout(() => {
|
||||
void fetchUnreadNotificationCount(token)
|
||||
.then((result) => {
|
||||
if (!cancelled) setUnreadCount(result.count ?? 0);
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}, 400);
|
||||
return () => {
|
||||
cancelled = true;
|
||||
window.clearTimeout(timer);
|
||||
};
|
||||
}, [isContentReady, isLoading, isPinLocked, token, user]);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!user || !token || isPinLocked || isLoading || !isContentReady) {
|
||||
socketRef.current?.close();
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
@@ -121,7 +140,7 @@ export function RealtimeProvider({ children }: { children: React.ReactNode }) {
|
||||
socketRef.current = null;
|
||||
setConnected(false);
|
||||
};
|
||||
}, [emit, isApiReady, isLoading, isPinLocked, token, user]);
|
||||
}, [emit, isContentReady, isLoading, isPinLocked, token, user]);
|
||||
|
||||
return (
|
||||
<RealtimeContext.Provider value={{ unreadCount, setUnreadCount, subscribe, connected }}>
|
||||
|
||||
@@ -12,12 +12,12 @@ interface AvatarAccessResponse {
|
||||
}
|
||||
|
||||
export function useAvatarUrl(userId: string | undefined, hasAvatar: boolean | undefined, token: string | null) {
|
||||
const { isPinLocked, isLoading, isApiReady } = useAuth();
|
||||
const { isPinLocked, isLoading, isContentReady } = useAuth();
|
||||
const [accessUrl, setAccessUrl] = useState<string | null>(null);
|
||||
const [resolvingAccessUrl, setResolvingAccessUrl] = useState(false);
|
||||
|
||||
const refreshAccessUrl = useCallback(async () => {
|
||||
if (!userId || !token || !hasAvatar || isPinLocked || isLoading || !isApiReady) {
|
||||
if (!userId || !token || !hasAvatar || isPinLocked || isLoading || !isContentReady) {
|
||||
setAccessUrl(null);
|
||||
setResolvingAccessUrl(false);
|
||||
return;
|
||||
@@ -31,7 +31,7 @@ export function useAvatarUrl(userId: string | undefined, hasAvatar: boolean | un
|
||||
} finally {
|
||||
setResolvingAccessUrl(false);
|
||||
}
|
||||
}, [hasAvatar, isApiReady, isLoading, isPinLocked, token, userId]);
|
||||
}, [hasAvatar, isContentReady, isLoading, isPinLocked, token, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
void refreshAccessUrl();
|
||||
@@ -59,7 +59,7 @@ export function useAvatarUrl(userId: string | undefined, hasAvatar: boolean | un
|
||||
isLoading: isLoadingBlob,
|
||||
hasError
|
||||
} = useResilientBlobUrl(accessUrl, token, {
|
||||
enabled: Boolean(hasAvatar && accessUrl && !isPinLocked && !isLoading && isApiReady),
|
||||
enabled: Boolean(hasAvatar && accessUrl && !isPinLocked && !isLoading && isContentReady),
|
||||
label: `аватар ${userId ?? ''}`.trim()
|
||||
});
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from '@/lib/api';
|
||||
|
||||
export function useSelectedFamily(enabled = true) {
|
||||
const { user, token, isPinLocked, isLoading, isApiReady } = useAuth();
|
||||
const { user, token, isPinLocked, isLoading, isContentReady } = useAuth();
|
||||
const { selectedGroupId, setSelectedGroupId } = useFamilyOverlay();
|
||||
const [groups, setGroups] = useState<FamilyGroup[]>([]);
|
||||
const [group, setGroup] = useState<FamilyGroup | null>(null);
|
||||
@@ -22,7 +22,7 @@ export function useSelectedFamily(enabled = true) {
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
const accessToken = getAccessToken() ?? token?.trim() ?? null;
|
||||
if (!enabled || !user || !accessToken || isPinLocked || isLoading || !isApiReady) {
|
||||
if (!enabled || !user || !accessToken || isPinLocked || isLoading || !isContentReady) {
|
||||
setGroups([]);
|
||||
setGroup(null);
|
||||
setPresenceMembers([]);
|
||||
@@ -62,7 +62,7 @@ export function useSelectedFamily(enabled = true) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [enabled, isApiReady, isLoading, isPinLocked, selectedGroupId, setSelectedGroupId, token, user]);
|
||||
}, [enabled, isContentReady, isLoading, isPinLocked, selectedGroupId, setSelectedGroupId, token, user]);
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(true);
|
||||
@@ -71,10 +71,10 @@ export function useSelectedFamily(enabled = true) {
|
||||
|
||||
useEffect(() => {
|
||||
const accessToken = getAccessToken() ?? token?.trim() ?? null;
|
||||
if (!enabled || !accessToken || isPinLocked || isLoading || !isApiReady || !group) return;
|
||||
if (!enabled || !accessToken || isPinLocked || isLoading || !isContentReady || !group) return;
|
||||
const timer = window.setInterval(() => void refresh(), 25_000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [enabled, group, isApiReady, isLoading, isPinLocked, refresh, token]);
|
||||
}, [enabled, group, isContentReady, isLoading, isPinLocked, refresh, token]);
|
||||
|
||||
const presenceByUserId = useMemo(
|
||||
() => new Map(presenceMembers.map((member) => [member.userId, member])),
|
||||
|
||||
@@ -677,10 +677,12 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
|
||||
}
|
||||
|
||||
const GATEWAY_RETRY_STATUS = new Set([502, 503, 504]);
|
||||
const GATEWAY_RETRY_ATTEMPTS = 4;
|
||||
const GATEWAY_RETRY_ATTEMPTS = 2;
|
||||
const GATEWAY_PROBE_MAX_ATTEMPTS = 48;
|
||||
const GATEWAY_PROBE_BASE_DELAY_MS = 450;
|
||||
const API_MAX_CONCURRENT = 8;
|
||||
const API_BURST_MAX_CONCURRENT = 3;
|
||||
const API_BURST_WINDOW_MS = 3000;
|
||||
const API_READY_EVENT = 'idp-api-ready';
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
@@ -689,6 +691,7 @@ function sleep(ms: number): Promise<void> {
|
||||
|
||||
let gatewayReadyResolved = false;
|
||||
let gatewayReadyPromise: Promise<void> | null = null;
|
||||
let gatewayReadyAt = 0;
|
||||
let activeApiRequests = 0;
|
||||
const apiRequestWaiters: Array<() => void> = [];
|
||||
|
||||
@@ -699,6 +702,7 @@ export function resetApiGatewayWarmup() {
|
||||
|
||||
export function invalidateApiGatewayReady() {
|
||||
gatewayReadyResolved = false;
|
||||
gatewayReadyAt = 0;
|
||||
}
|
||||
|
||||
export function isApiGatewayReady() {
|
||||
@@ -708,6 +712,7 @@ export function isApiGatewayReady() {
|
||||
export function markApiGatewayReady() {
|
||||
if (gatewayReadyResolved) return;
|
||||
gatewayReadyResolved = true;
|
||||
gatewayReadyAt = Date.now();
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(new CustomEvent(API_READY_EVENT));
|
||||
}
|
||||
@@ -724,13 +729,14 @@ export function subscribeApiReady(listener: () => void): () => void {
|
||||
}
|
||||
|
||||
async function acquireApiSlot(): Promise<void> {
|
||||
if (activeApiRequests < API_MAX_CONCURRENT) {
|
||||
activeApiRequests += 1;
|
||||
return;
|
||||
}
|
||||
const inBurst = gatewayReadyAt > 0 && Date.now() - gatewayReadyAt < API_BURST_WINDOW_MS;
|
||||
const limit = inBurst ? API_BURST_MAX_CONCURRENT : API_MAX_CONCURRENT;
|
||||
|
||||
while (activeApiRequests >= limit) {
|
||||
await new Promise<void>((resolve) => {
|
||||
apiRequestWaiters.push(resolve);
|
||||
});
|
||||
}
|
||||
activeApiRequests += 1;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user