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

Ответ

Под "восстановлением ресурсов" в 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 для хранения уже загруженных данных. При повторном открытии приложения или вкладки, сначала отображались данные из кеша состояния, а затем в фоне происходил запрос на актуализацию. Это создавало ощущение мгновенной загрузки и позволяло работать оффлайн.

Ответ 18+ 🔞

Э, слушай, история-то простая, но охуенно важная. Когда твой клиент пытается вытянуть дохуя данных, а сеть подводит — это пиздец как бесит. Вот как я обычно делаю, чтобы не было потом истерики у пользователей.

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. Механизм повторных попыток — без этого нихуя. Сеть — она, блядь, такая хитрая жопа. Сегодня есть, завтра — хуй с горы. Поэтому на клиенте надо делать запросы с повторными попытками. А то пользователь ткнет раз, ошибка 500, и он уже в истерике, думает, что всё накрылось медным тазом.

// Утилитарная функция для запросов с повторными попытками
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. Для больших файлов — чанковая загрузка. Пытаться залить гигабайтный файл одним запросом — это, ёпта, чистой воды самоубийство. Обрыв на 99% — и привет, начинай сначала, терпения ноль ебать. Я использовал tus-js-client на фронте и tus-node-server на бэке. Эта штука умеет возобновлять загрузку с места обрыва. Просто космос, а не технология.

4. Кеширование в Service Worker. Чтобы даже при полном отсутствии сети пользователь не смотрел в пустой экран, как дурак. Реализуешь стратегию "Cache then Network" — сначала показываешь старые, закешированные данные (пусть даже вчерашние), а потом в фоне тихонько подтягиваешь свежие. Волнение ебать у пользователя сразу спадает.

5. Сохранение состояния на клиенте. Zustand, Context API — неважно. Суть в том, чтобы при повторном открытии приложения у тебя сразу отображалось то, что ты уже видел. Создаётся ощущение, что всё грузится мгновенно. Пока ты моргнул, данные уже на экране, а на сервере только запрос ушёл на актуализацию. Пользователь доволен, а доверия ебать ноль превращается в "ого, как быстро".

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