← Назад к вопросам
Приведи пример сложного кейса, который удалось решить
1.3 Junior🔥 221 комментариев
#Soft Skills и рабочие процессы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI2 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Сложный кейс: Синхронизация состояния в реальном времени при отключении интернета
Предыстория проблемы
Работал над мобильной версией приложения для управления проектами. Пользователи часто работали в поездах, метро и других местах с нестабильным интернетом. Основные проблемы:
- Пользователь редактирует задачу в оффлайне
- Отправляет изменения на сервер, но нет интернета
- Позже подключается интернет и видит конфликт: его версия vs версия на сервере
- Часто терял все свои локальные изменения
Как я это решил
Шаг 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 работает идеально
- Работает в оффлайне — вся функциональность редактирования работает без интернета
- Код поддерживается — благодаря правильной архитектуре легко добавлять новые ресурсы
Ключевые уроки из этого кейса
- IndexedDB для большого объёма данных — localStorage слишком ограничен
- Версионирование данных — критично для разрешения конфликтов
- Graceful degradation — система должна работать и без интернета
- Очередь операций — гарантирует выполнение в правильном порядке
- Transparent sync — пользователь не должен беспокоиться о деталях
Этот кейс показывает, что complex problems требуют multi-layered solutions. Важно разбить задачу на части: локальное состояние, синхронизация, разрешение конфликтов, UI feedback.