Таймауты

Работа с асинхронной валидацией в Vest неизбежно приводит к необходимости управления временем выполнения правил, особенно когда проверки зависят от сети, внешних сервисов или нестабильных источников данных. Таймауты становятся механизмом, который ограничивает длительность ожидания результата и защищает форму от зависания состояния валидации.

Асинхронные проверки в Vest часто используются для сценариев, где синхронная логика недостаточна: проверка уникальности логина, доступности email, валидности промокода через API. В таких условиях задержка ответа может быть вызвана:

  • сетевой латентностью;
  • перегрузкой API;
  • нестабильным соединением;
  • внутренними очередями сервера;
  • ограничениями rate limit.

Без контроля времени выполнения каждая такая проверка становится потенциальным источником неопределённого ожидания. Таймаут формирует верхнюю границу этого ожидания.

Модель таймаута в асинхронной валидации

Таймаут в контексте Vest — это не просто задержка выполнения, а механизм управления жизненным циклом промиса. Он определяет момент, после которого результат проверки считается недействительным, даже если исходный запрос ещё не завершён.

Типовая модель включает три состояния:

  • ожидание результата проверки;
  • успешное завершение в пределах времени;
  • принудительное завершение по истечении лимита.

Ключевая особенность заключается в том, что таймаут не отменяет сам запрос автоматически во всех средах, но блокирует его влияние на состояние валидации.

Использование таймаутов в async-правилах

Асинхронные правила в Vest строятся через test с поддержкой промисов. Таймаут реализуется на уровне логики выполнения.

Пример базовой структуры:

import { create, test, enforce } from 'vest';

const suite = create((data) => {
  test('username', 'Проверка логина', async () => {
    enforce(data.username).isNotEmpty();

    await checkAvailabilityWithTimeout(data.username, 2000);
  });
});

Функция checkAvailabilityWithTimeout обычно инкапсулирует логику ограничения времени.

Реализация таймаута через Promise.race

Наиболее распространённый подход — использование Promise.race, где конкурируют основной запрос и таймер.

function timeout(ms) {
  return new Promise((_, reject) =>
    setTimeout(() => reject(new Error('TIMEOUT')), ms)
  );
}

function checkAvailability(username) {
  return fetch(`/api/users/check?name=${username}`)
    .then(res => res.json());
}

function checkAvailabilityWithTimeout(username, ms) {
  return Promise.race([
    checkAvailability(username),
    timeout(ms)
  ]);
}

Такой подход обеспечивает детерминированное поведение: либо приходит ответ сервера, либо срабатывает ограничение времени.

Обработка таймаута внутри Vest-правил

При интеграции с Vest важно различать:

  • ошибку валидации;
  • сетевую ошибку;
  • таймаут как отдельное состояние.

Таймаут обычно не равен “невалидному значению”. Он скорее отражает невозможность подтвердить валидность.

test('email', 'Проверка email', async () => {
  try {
    await checkEmail(data.email);
  } catch (e) {
    if (e.message === 'TIMEOUT') {
      test.fail('Сервис проверки недоступен');
    } else {
      test.fail('Ошибка проверки email');
    }
  }
});

Такая дифференциация позволяет отделить UX-ошибки инфраструктуры от логических ошибок ввода.

Деградация поведения при таймаутах

Системы валидации часто проектируются с учётом деградации функциональности. Таймаут становится триггером перехода в упрощённый режим:

  • отключение строгой проверки;
  • использование кешированных результатов;
  • временное принятие значения с пометкой “не подтверждено”.

В Vest это реализуется через условную логику внутри suite:

let fallbackMode = false;

test('username', 'Username check', async () => {
  if (fallbackMode) return;

  try {
    await checkAvailabilityWithTimeout(data.username, 1500);
  } catch {
    fallbackMode = true;
  }
});

Такой подход снижает нагрузку на внешние сервисы и предотвращает блокировку пользовательского ввода.

Таймауты и race conditions

При параллельной валидации нескольких полей возникает проблема устаревших ответов. Таймауты частично решают её, но не устраняют полностью.

Типичный сценарий:

  1. пользователь вводит значение;
  2. запускается асинхронная проверка;
  3. пользователь изменяет значение;
  4. предыдущий запрос возвращается позже нового.

Без защиты состояние может быть перезаписано устаревшим результатом. Таймаут в этом случае используется вместе с идентификаторами запросов:

let currentRequestId = 0;

test('username', async () => {
  const requestId = ++currentRequestId;

  const result = await checkAvailability(data.username);

  if (requestId !== currentRequestId) return;

  enforce(result.available).isTruthy();
});

Хотя это не таймаут в прямом смысле, он дополняет стратегию контроля времени жизни запроса.

Комбинация debounce и таймаутов

Валидация часто страдает от избыточных запросов при вводе. Таймауты в связке с debounce формируют устойчивую систему контроля нагрузки.

Debounce ограничивает частоту запуска:

  • пользователь вводит текст;
  • проверка запускается только после паузы.

Таймаут ограничивает длительность самой проверки.

function debounce(fn, delay) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), delay);
  };
}

В сочетании:

  • debounce снижает количество запросов;
  • timeout ограничивает длительность каждого запроса.

Таймауты в конкурентной валидации полей

В формах с множеством зависимых полей таймауты позволяют избежать блокировки всей схемы проверки. Если одно поле зависает, остальные продолжают проверяться.

Vest выполняет тесты декларативно, поэтому важно изолировать потенциально “долгие” проверки:

test('promo', async () => {
  await Promise.race([
    validatePromo(data.promo),
    timeout(1000)
  ]);
});

Даже при сбое внешнего API остальные правила продолжают выполняться.

Ошибки проектирования таймаутов

Неправильное использование таймаутов приводит к системным проблемам:

  • слишком короткие интервалы создают ложные ошибки;
  • слишком длинные — теряют смысл ограничения;
  • отсутствие логирования делает поведение непредсказуемым;
  • игнорирование отмены устаревших запросов приводит к гонкам состояния.

Особенно критично сочетание таймаута с повторными попытками, когда система может входить в бесконечный цикл повторений без контроля нагрузки.

Стратегии устойчивой работы

В зрелых реализациях таймауты рассматриваются как часть общей политики устойчивости:

  • экспоненциальный backoff при повторных запросах;
  • кеширование успешных ответов;
  • ограничение параллельных проверок;
  • централизованная обработка ошибок сети;
  • единый слой абстракции над API-запросами.

Такая архитектура снижает зависимость формы от внешних факторов и делает поведение предсказуемым даже при деградации сервисов.

Связь таймаутов с пользовательским вводом

Хотя таймауты технически относятся к инфраструктуре, их влияние напрямую отражается на взаимодействии с интерфейсом. Задержки валидации воспринимаются как “торможение формы”, поэтому ограничения времени выполнения становятся инструментом UX-оптимизации.

Стабильная система таймаутов обеспечивает:

  • отсутствие зависших состояний;
  • предсказуемую обратную связь;
  • быстрое завершение неуспешных проверок;
  • снижение визуальной неопределённости при вводе данных.