Типизация и детерминированная обработка race condition при конкурентной записи в WebSoc...
Типизация и детерминированная обработка race condition при конкурентной записи в WebSocket через AbortController и AsyncQueue в React 19 Race condition в WebSocket-соединениях — не просто теоретическая проблема. В production она возникает, когда несколько компонентов одновременно пытаются отправить данные: два сабмита формы, два useEffect с разными контекстами. Сообщения перемешиваются, теряются, или соединение падает с нечитаемым traceback. Стандартный WebSocket не гарантирует порядок, если не управлять очередью вручную. Почему это важно для React 19 React 19 не предоставляет магического решения для WebSocket. useCallback и AbortController — стандартные API, работающие с React 16, но в 19-й версии они стабильны, и связка с очередью чувствует себя уверенно. Однако race condition остаётся: два вызова send из разных компонентов без синхронизации приводят к хаосу. Решение: AsyncQueue и AbortController Берём самописный AsyncQueue с FIFO и AbortController для отмены устаревших задач. Очередь гарантирует, что следующее сообщение отправится только после получения ответа на предыдущее. AbortSignal позволяет скипать задачу, если компонент размонтировался или пользователь передумал. Вот типизированный пример: type SendTask = { data: unknown; signal: AbortSignal; }; class AsyncQueue { private queue: SendTask[] = []; private processing = false; async enqueue(task: SendTask): Promise<void> { this.queue.push(task); if (!this.processing) { this.processing = true; while (this.queue.length > 0) { const current = this.queue.shift()!; if (current.signal.aborted) continue; await this.sendToWS(current.data); } this.processing = false; } } private async sendToWS(data: unknown): Promise<void> { await new Promise<void>((resolve, reject) => { const timeout = setTimeout(() => reject(new Error('Timeout')), 5000); ws.send(JSON.stringify(data)); ws.once('message', () => { clearTimeout(timeout); resolve(); }); }); } } const useSafeWS = () => { const queueRef = useRef(new AsyncQueue()); const send = useCallback((data: unknown, signal?: AbortSignal) => { const controller = new AbortController(); const effectiveSignal = signal || controller.signal; queueRef.current.enqueue({ data, signal: effectiveSignal }); if (!signal) controller.abort(); }, []); return { send }; }; Типичная ошибка: игнорирование таймаутов и очистки Главный подводный камень — если сокет завис на ожидании ответа, вся очередь блокируется. В production добавьте fallback на переподключение: при таймауте разрывайте соединение, очищайте очередь и инициируйте reconnect. Без этого компоненты будут ждать вечно, а состояние интерфейса застынет. Trade-off: самописное vs библиотечное решение Самописная очередь требует тестирования edge cases: таймауты, параллельные подключения, отмена задач. Библиотеки вроде ws-queue решают это из коробки, но добавляют зависимость и скрывают логику. Я выбираю своё — прозрачность дебага и ноль зависимостей. Минус: нужно покрыть тестами сценарии с зависанием и переподключением. Вывод: Race condition в WebSocket решается через FIFO-очередь с AbortController, но не забывайте про таймауты и fallback на reconnect, иначе детерминизм превратится в блокировку.