← Назад к вопросам

Приведи пример сложного кейса, который удалось решить

1.3 Junior🔥 221 комментариев
#Soft Skills и рабочие процессы

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Сложный кейс: Синхронизация состояния в реальном времени при отключении интернета

Предыстория проблемы

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

  1. Пользователь редактирует задачу в оффлайне
  2. Отправляет изменения на сервер, но нет интернета
  3. Позже подключается интернет и видит конфликт: его версия vs версия на сервере
  4. Часто терял все свои локальные изменения

Как я это решил

Шаг 1: Локальное кеширование и очередь операций

// Создал систему для кеша операций в IndexedDB
interface PendingOperation {
  id: string;
  type: 'CREATE' | 'UPDATE' | 'DELETE';
  resource: 'task' | 'comment';
  data: Record<string, any>;
  timestamp: number;
  retries: number;
}

// Хук для управления состоянием с локальным кешем
export function useOfflineSyncedState<T>(
  key: string,
  initialValue: T,
  syncFunction: (data: T) => Promise<void>
) {
  const [data, setData] = useState<T>(initialValue);
  const [isSyncing, setIsSyncing] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  // Сохраняем в IndexedDB при каждом изменении
  const updateData = useCallback(async (newData: T) => {
    setData(newData);

    // Сохраняем локально
    const db = await openDB('app-cache');
    await db.put('pending-operations', {
      id: `${key}-${Date.now()}`,
      type: 'UPDATE',
      resource: key,
      data: newData,
      timestamp: Date.now(),
      retries: 0,
    });
  }, [key]);

  // Синхронизируем с сервером при наличии интернета
  useEffect(() => {
    const handleOnline = async () => {
      try {
        setIsSyncing(true);

        // Получаем все pending операции
        const db = await openDB('app-cache');
        const operations = await db.getAll('pending-operations');

        // Сортируем по timestamp (выполняем в порядке создания)
        operations.sort((a, b) => a.timestamp - b.timestamp);

        // Выполняем каждую операцию
        for (const op of operations) {
          try {
            await syncFunction(op.data);
            // Удаляем из очереди только после успеха
            await db.delete('pending-operations', op.id);
          } catch (err) {
            // Увеличиваем количество попыток
            op.retries++;
            if (op.retries > 5) {
              // После 5 попыток просим пользователя разобраться
              setError(err as Error);
              break;
            }
          }
        }

        setIsSyncing(false);
      } catch (err) {
        setError(err as Error);
        setIsSyncing(false);
      }
    };

    window.addEventListener('online', handleOnline);
    return () => window.removeEventListener('online', handleOnline);
  }, [syncFunction]);

  return { data, updateData, isSyncing, error };
}

Шаг 2: Конфликт разрешение (Operational Transformation)

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

interface Task {
  id: string;
  title: string;
  description: string;
  version: number; // Версия на сервере
  localVersion: number; // Версия в клиенте
}

// Функция для разрешения конфликтов
async function resolveConflict(
  localTask: Task,
  serverTask: Task
): Promise<Task> {
  // Стратегия 1: Server wins (просто используем данные с сервера)
  if (serverTask.version > localTask.localVersion) {
    return { ...serverTask, localVersion: serverTask.version };
  }

  // Стратегия 2: Merge по полям (более продвинуто)
  // Какие поля изменил пользователь локально?
  const localChanges = detectChanges(localTask, serverTask);

  // Объединяем изменения
  const mergedTask = { ...serverTask };
  for (const field of Object.keys(localChanges)) {
    if (field !== 'version' && field !== 'localVersion') {
      (mergedTask as any)[field] = (localTask as any)[field];
    }
  }

  return { ...mergedTask, localVersion: serverTask.version };
}

function detectChanges(
  localVersion: Task,
  serverVersion: Task
): Record<string, boolean> {
  const changes: Record<string, boolean> = {};
  const fields = ['title', 'description'];

  for (const field of fields) {
    const localVal = (localVersion as any)[field];
    const serverVal = (serverVersion as any)[field];
    if (localVal !== serverVal) {
      changes[field] = true;
    }
  }

  return changes;
}

Шаг 3: UI для показа статуса синхронизации

export function TaskEditor() {
  const {
    data: task,
    updateData: updateTask,
    isSyncing,
    error,
  } = useOfflineSyncedState('task', initialTask, syncTaskToServer);

  return (
    <div className="space-y-4">
      {/* Статус синхронизации */}
      {isSyncing && (
        <div className="flex items-center gap-2 text-content-2 bg-blue-50 p-3 rounded-lg">
          <div className="w-4 h-4 bg-blue-500 rounded-full animate-pulse" />
          Syncing changes...
        </div>
      )}

      {error && (
        <div className="flex items-center gap-2 text-red-600 bg-red-50 p-3 rounded-lg">
          <svg className="w-5 h-5">
            {/* Error icon */}
          </svg>
          <div>
            <p className="font-semibold">Failed to sync</p>
            <p className="text-sm">{error.message}</p>
            <button
              onClick={() => window.dispatchEvent(new Event('online'))}
              className="text-sm underline mt-1"
            >
              Retry
            </button>
          </div>
        </div>
      )}

      {/* Статус интернета */}
      <div className="text-sm text-content-2">
        {navigator.onLine ? 'Online' : 'Offline - changes will sync when online'}
      </div>

      {/* Форма редактирования */}
      <input
        type="text"
        value={task.title}
        onChange={(e) =>
          updateTask({ ...task, title: e.target.value })
        }
        placeholder="Task title"
        className="w-full p-2 border border-border-1 rounded-lg"
      />
      <textarea
        value={task.description}
        onChange={(e) =>
          updateTask({ ...task, description: e.target.value })
        }
        placeholder="Task description"
        className="w-full p-2 border border-border-1 rounded-lg min-h-24"
      />
    </div>
  );
}

Шаг 4: WebSocket с fallback на polling

Для более надёжной синхронизации добавил WebSocket:

export function useRealtimeSync<T>(
  key: string,
  onRemoteChange: (data: T) => void
) {
  const wsRef = useRef<WebSocket | null>(null);
  const reconnectTimeoutRef = useRef<NodeJS.Timeout>();

  useEffect(() => {
    const connectWebSocket = () => {
      try {
        wsRef.current = new WebSocket(`wss://api.example.com/ws?channel=${key}`);

        wsRef.current.onmessage = (event) => {
          const { type, payload } = JSON.parse(event.data);
          if (type === 'DATA_CHANGED') {
            onRemoteChange(payload);
          }
        };

        wsRef.current.onclose = () => {
          // Пытаемся переподключиться через 3 секунды
          reconnectTimeoutRef.current = setTimeout(connectWebSocket, 3000);
        };

        wsRef.current.onerror = () => {
          // Fallback на polling если WebSocket не работает
          const pollInterval = setInterval(async () => {
            try {
              const data = await fetch(
                `/api/v1/${key}?since=${Date.now() - 5000}`
              ).then((r) => r.json());
              onRemoteChange(data);
            } catch (err) {
              // Игнорируем ошибки polling
            }
          }, 5000);

          return () => clearInterval(pollInterval);
        };
      } catch (err) {
        console.error('WebSocket connection failed:', err);
      }
    };

    connectWebSocket();

    return () => {
      wsRef.current?.close();
      clearTimeout(reconnectTimeoutRef.current);
    };
  }, [key, onRemoteChange]);
}

Результаты

  • 0% потери данных — все изменения сохраняются локально и синхронизируются
  • Плавный UX — пользователь видит, что идёт синхронизация, но может продолжать работу
  • Автоматическое разрешение конфликтов — в 95% случаев merge работает идеально
  • Работает в оффлайне — вся функциональность редактирования работает без интернета
  • Код поддерживается — благодаря правильной архитектуре легко добавлять новые ресурсы

Ключевые уроки из этого кейса

  1. IndexedDB для большого объёма данных — localStorage слишком ограничен
  2. Версионирование данных — критично для разрешения конфликтов
  3. Graceful degradation — система должна работать и без интернета
  4. Очередь операций — гарантирует выполнение в правильном порядке
  5. Transparent sync — пользователь не должен беспокоиться о деталях

Этот кейс показывает, что complex problems требуют multi-layered solutions. Важно разбить задачу на части: локальное состояние, синхронизация, разрешение конфликтов, UI feedback.

Приведи пример сложного кейса, который удалось решить | PrepBro