Асинхронная валидация

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

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


Асинхронная логика в Superstruct чаще всего строится вокруг пользовательских структур, созданных через define, а также расширения поведения через refine, где допускается возврат Promise.

import { define } fr om 'superstruct'

const UniqueUsername = define('UniqueUsername', async (value) => {
  const response = await fetch(`/api/check-username?name=${value}`)
  const data = await response.json()

  return data.available === true
})

В этом примере структура возвращает true или false, но фактически выполнение происходит асинхронно. Это делает возможным подключение внешних сервисов без изменения общей архитектуры валидации.


Ограничения синхронного ядра

Стандартная функция assert в Superstruct работает синхронно:

import { assert, string } from 'superstruct'

assert('hello', string())

Попытка напрямую использовать await внутри стандартных структур невозможна. Поэтому асинхронная логика всегда выносится в пользовательские структуры или в этапы предварительной обработки.

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


Использование validate с Promise

Функция validate поддерживает асинхронный сценарий через возвращаемый Promise.

import { validate, string } from 'superstruct'

async function runValidation(value) {
  const [error, result] = await validate(value, string())

  if (error) {
    console.log('Ошибка валидации')
    return
  }

  console.log('Корректное значение:', result)
}

При подключении пользовательских асинхронных структур validate автоматически начинает возвращать промис, что позволяет строить цепочки проверок.


Асинхронный refine как основной механизм расширения

refine используется для добавления дополнительной логики проверки поверх базовой структуры.

import { string, refine } from 'superstruct'

const Email = refine(string(), 'Email', async (value) => {
  const response = await fetch(`/api/check-email?email=${value}`)
  const result = await response.json()

  return result.valid
})

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


Композиция асинхронных структур

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

import { object, string } from 'superstruct'

const User = object({
  name: string(),
  email: Email
})

async function validateUser(data) {
  const [error, value] = await validate(data, User)

  if (error) {
    throw error
  }

  return value
}

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


Паттерн разделения синхронной и асинхронной валидации

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

Синхронный слой

import { string, size } from 'superstruct'

const UsernameBase = size(string(), 3, 20)

Асинхронный слой

import { refine } from 'superstruct'

const Username = refine(UsernameBase, 'Username', async (value) => {
  const res = await fetch(`/api/username/${value}`)
  const data = await res.json()

  return data.available
})

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


Обработка ошибок в асинхронной валидации

Ошибки в асинхронных структурах могут возникать как на уровне сети, так и на уровне логики проверки.

const SafeEmail = refine(string(), 'SafeEmail', async (value) => {
  try {
    const res = await fetch(`/api/email-check?email=${value}`)

    if (!res.ok) {
      return false
    }

    const data = await res.json()
    return data.allowed
  } catch (e) {
    return false
  }
})

Здесь важно учитывать, что возврат false трактуется как провал валидации, а не как исключение.


Асинхронная валидация массивов и вложенных структур

При работе с массивами каждый элемент может требовать отдельной проверки, включая асинхронную.

import { array, string } from 'superstruct'

const CheckedItems = array(
  refine(string(), 'ItemCheck', async (value) => {
    const res = await fetch(`/api/item/${value}`)
    const data = await res.json()
    return data.exists
  })
)

Каждый элемент массива проходит собственный асинхронный цикл, что может повлиять на производительность при больших объёмах данных.


Параллельная и последовательная стратегия проверки

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

Параллельная модель

await Promise.all(values.map(v => validate(v, Struct)))

Последовательная модель

for (const v of values) {
  await validate(v, Struct)
}

Параллельная стратегия уменьшает общее время, но увеличивает нагрузку на внешние API. Последовательная — более стабильна при ограничениях по rate lim it.


Интеграция с бизнес-логикой

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

const CanCreatePost = refine(string(), 'CanCreatePost', async (userId) => {
  const res = await fetch(`/api/users/${userId}/permissions`)
  const data = await res.json()

  return data.canCreatePost
})

Такие структуры фактически объединяют слой валидации и слой авторизации.


Производительность и кэширование результатов

Асинхронные проверки могут создавать значительную нагрузку при повторяющихся запросах. Для оптимизации применяется кэширование:

const cache = new Map()

const CachedUsernameCheck = refine(string(), 'CachedUsername', async (value) => {
  if (cache.has(value)) {
    return cache.get(value)
  }

  const res = await fetch(`/api/check/${value}`)
  const data = await res.json()

  cache.set(value, data.available)

  return data.available
})

Кэширование снижает количество внешних запросов и стабилизирует поведение системы.


Взаимодействие с обработкой форм

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

async function submit(formData) {
  const [error, value] = await validate(formData, User)

  if (error) {
    return { success: false, error }
  }

  await fetch('/api/create-user', {
    method: 'POST',
    body: JSON.stringify(value)
  })

  return { success: true }
}

Асинхронный характер позволяет встроить валидацию непосредственно в поток выполнения без блокировки интерфейса или логики приложения.