Retry-логика

В SkateJS подход к реализации повторных попыток (retry) при работе с асинхронными процессами строится на принципах реактивного обновления состояния компонентов и управления жизненным циклом веб-компонентов. В отличие от традиционных библиотек, где retry реализуется через простые циклы или утилиты типа setTimeout, SkateJS интегрирует такие механизмы прямо в поведение компонентов, позволяя управлять состоянием, повторными запросами и обработкой ошибок на уровне свойств и реактивных обновлений.

Асинхронные свойства и retry

Основной инструмент для реализации повторных попыток — асинхронные свойства (asyncProp). Они позволяют описывать свойства компонента, которые вычисляются асинхронно, и автоматически инициируют перерисовку при изменении значений.

Пример объявления асинхронного свойства:

import { Component, prop } from 'skatejs';

class AsyncComponent extends Component {
  static props = {
    data: prop({
      attribute: true,
      async: true
    })
  };

  async fetchData() {
    const response = await fetch('https://api.example.com/data');
    if (!response.ok) {
      throw new Error('Ошибка запроса');
    }
    return response.json();
  }
}

Для внедрения retry-логики в асинхронное свойство можно обернуть асинхронную функцию в отдельный метод, который реализует повторные попытки при отказах:

async retryFetch(retries = 3, delay = 1000) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await this.fetchData();
    } catch (error) {
      if (attempt === retries) throw error;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

В этом подходе ключевые моменты:

  • Количество попыток задаётся через параметр retries.
  • Задержка между попытками через delay позволяет избежать излишней нагрузки на сервер.
  • Ошибки, достигшие последней попытки, пробрасываются наружу для обработки в компоненте.

Интеграция retry с жизненным циклом

SkateJS предоставляет методы жизненного цикла компонента, такие как connectedCallback и updated. Retry-логику удобно интегрировать в них, чтобы инициировать асинхронные операции после монтирования компонента:

class RetryComponent extends Component {
  static props = {
    data: prop({ async: true })
  };

  connectedCallback() {
    super.connectedCallback();
    this.loadData();
  }

  async loadData() {
    try {
      this.data = await this.retryFetch(5, 2000);
    } catch (error) {
      console.error('Не удалось загрузить данные:', error);
    }
  }
}

Такой подход позволяет:

  • Автоматически инициировать повторные попытки при монтировании компонента.
  • Обновлять свойства компонента (this.data) только при успешном завершении запроса.
  • Локально обрабатывать ошибки, не нарушая работу остальной части приложения.

Параметризация retry-логики

Для повышения гибкости часто выносят retry-параметры в свойства компонента:

static props = {
  data: prop({ async: true }),
  retries: prop({ default: 3 }),
  delay: prop({ default: 1000 })
};

async loadData() {
  try {
    this.data = await this.retryFetch(this.retries, this.delay);
  } catch (error) {
    console.error('Ошибка загрузки данных:', error);
  }
}

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

Экспоненциальное увеличение задержки

Для улучшения устойчивости при частых сбоях полезно использовать экспоненциальный backoff:

async retryFetch(retries = 3, delay = 1000) {
  for (let attempt = 1; attempt <= retries; attempt++) {
    try {
      return await this.fetchData();
    } catch (error) {
      if (attempt === retries) throw error;
      await new Promise(resolve => setTimeout(resolve, delay));
      delay *= 2; // экспоненциальное увеличение задержки
    }
  }
}

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

Обработка ошибок и fallback

Важной частью retry-логики является fallback — запасной вариант действий при постоянных ошибках:

async loadData() {
  try {
    this.data = await this.retryFetch(5, 1000);
  } catch {
    this.data = []; // fallback: пустой массив вместо данных
  }
}

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

Совмещение с рендерингом

SkateJS позволяет реактивно обновлять DOM при изменении свойства. Использование retry напрямую влияет на рендеринг:

render() {
  if (!this.data) return `<p>Загрузка...</p>`;
  if (this.data.length === 0) return `<p>Нет данных</p>`;
  return `<ul>${this.data.map(item => `<li>${item.name}</li>`).join('')}</ul>`;
}

Комбинируя retry-логику и реактивное свойство, можно создавать компоненты, которые:

  • Автоматически повторяют запросы при ошибках.
  • Отображают прогресс загрузки и fallback-контент.
  • Гибко настраиваются через параметры компонента.

Итоговая структура компонента с retry

class RetryExample extends Component {
  static props = {
    data: prop({ async: true }),
    retries: prop({ default: 3 }),
    delay: prop({ default: 1000 })
  };

  connectedCallback() {
    super.connectedCallback();
    this.loadData();
  }

  async fetchData() {
    const res = await fetch('https://api.example.com/data');
    if (!res.ok) throw new Error('Ошибка сети');
    return res.json();
  }

  async retryFetch(retries, delay) {
    for (let i = 1; i <= retries; i++) {
      try {
        return await this.fetchData();
      } catch (error) {
        if (i === retries) throw error;
        await new Promise(resolve => setTimeout(resolve, delay));
        delay *= 2;
      }
    }
  }

  async loadData() {
    try {
      this.data = await this.retryFetch(this.retries, this.delay);
    } catch {
      this.data = [];
    }
  }

  render() {
    if (!this.data) return `<p>Загрузка...</p>`;
    if (this.data.length === 0) return `<p>Нет данных</p>`;
    return `<ul>${this.data.map(item => `<li>${item.name}</li>`).join('')}</ul>`;
  }
}
customElements.define('retry-example', RetryExample);

Такой компонент демонстрирует комплексное применение retry-логики: асинхронные свойства, параметры повторных попыток, экспоненциальный backoff, обработку ошибок и реактивный рендеринг данных.