Как реализовать восстановление ресурсов с сервера

«Как реализовать восстановление ресурсов с сервера» — вопрос из категории Архитектура, который задают на 26% собеседований Node.js Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Под "восстановлением ресурсов" в Node.js я понимаю стратегии для эффективной и отказоустойчивой загрузки данных клиентом, особенно после сбоев или для больших объёмов данных. Вот как я это реализовывал:

1. Пагинация с курсорами для стабильности: Вместо offset/limit пагинации, которая может "плыть" при добавлении новых данных, использовал курсорную пагинацию на основе времени создания или уникального ID.

// API Endpoint в Express
app.get('/api/transactions', async (req, res) => {
  const limit = parseInt(req.query.limit) || 50;
  const cursor = req.query.cursor; // ID последней полученной записи

  let query = {};
  if (cursor) {
    // Находим запись по курсору и выбираем следующие
    const lastItem = await Transaction.findById(cursor);
    if (lastItem) {
      query.createdAt = { $lt: lastItem.createdAt };
    }
  }

  const transactions = await Transaction.find(query)
    .sort({ createdAt: -1 })
    .limit(limit + 1); // Берём на одну больше, чтобы проверить, есть ли ещё данные

  const hasNextPage = transactions.length > limit;
  const items = hasNextPage ? transactions.slice(0, -1) : transactions;
  const nextCursor = hasNextPage ? items[items.length - 1]._id : null;

  res.json({
    items,
    pageInfo: {
      hasNextPage,
      nextCursor
    }
  });
});

2. Механизм повторных попыток (Retry Logic) на клиенте: При временном сбое сети или ошибке 5xx сервера, фронтенд автоматически повторяет запрос.

// Утилитарная функция для запросов с повторными попытками
async function fetchWithRetry(url, options = {}, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch(url, options);
      if (!response.ok) throw new Error(`HTTP ${response.status}`);
      return await response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error; // Последняя попытка
      // Экспоненциальная задержка перед повторной попыткой
      const delay = Math.pow(2, i) * 1000 + Math.random() * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
      console.warn(`Попытка ${i + 1} не удалась, повтор через ${delay}мс`);
    }
  }
}

// Использование
const data = await fetchWithRetry('/api/large-dataset');

3. Чанковая загрузка (Chunked Upload/Download) для больших файлов: Для восстановления прерванной загрузки файла использовал библиотеку tus-js-client на фронтенде и tus-node-server на бэкенде, которая поддерживает возобновляемую загрузку.

4. Кеширование ответов API в Service Worker: Реализовывал стратегию "Cache then Network" для критичных данных, чтобы при отсутствии сети пользователь видел закешированную, пусть и устаревшую, версию.

5. Сохранение состояния на клиенте: Использовал Zustand или Context API в React для хранения уже загруженных данных. При повторном открытии приложения или вкладки, сначала отображались данные из кеша состояния, а затем в фоне происходил запрос на актуализацию. Это создавало ощущение мгновенной загрузки и позволяло работать оффлайн.