/** A synchronous lock: React state alone cannot prevent two calls in one tick. */ export class OperationGate { private keys = new Set() acquire(key: string): boolean { if (this.keys.has(key)) return false this.keys.add(key) return true } release(key: string) { this.keys.delete(key) } has(key: string) { return this.keys.has(key) } } export type PollResult = { id: string; error: string | null } export function serverOperationError(error: unknown): string { if (error && typeof error === "object" && "status" in error && error.status === 403) { return "Недостаточно прав для этого действия." } if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError")) { return "Время ожидания истекло. Результат операции неизвестен — обновите данные перед повтором." } if (error instanceof TypeError) return "Нет соединения с сервером. Проверьте подключение и обновите данные." return error instanceof Error ? error.message : "Не удалось выполнить действие. Попробуйте ещё раз." } /** Bounded polling, preserving one outcome for every requested server. */ export async function pollServers( ids: string[], poll: (id: string) => Promise, ): Promise { const uniqueIds = [...new Set(ids)] const results: PollResult[] = new Array(uniqueIds.length) let next = 0 await Promise.all(Array.from({ length: Math.min(4, uniqueIds.length) }, async () => { while (next < uniqueIds.length) { const index = next++ const id = uniqueIds[index] try { await poll(id) results[index] = { id, error: null } } catch (error) { results[index] = { id, error: serverOperationError(error) } } } })) return results }