Клиентская часть гонки (вариант B): Web Locks API (navigator.locks) — кросс-вкладочный мьютекс iistwin-offline-sync-replay на replay-проход в useOfflineSync: первая вкладка под локом реплеит и удаляет записи, остальные видят пустую очередь. Fallback для сред без Web Locks — старое поведение (дубли гасит серверная идемпотентность). Race-тест: без locks → 2 POST, с locks → 1 POST для одной записи очереди. Мессенджер (conv_messages) — та же защита, что у task_messages: - conversation_messages.client_message_id + partial unique (миграция 0087); - POST /api/messenger/conversations/:id/messages: повтор с тем же clientMessageId → существующее сообщение без insert и без SSE/уведомлений, гонка insert'ов ловится по 23505; - useChatController шлёт crypto.randomUUID(), attachment-реплей мессенджера тоже использует id записи очереди.
11 lines
706 B
SQL
11 lines
706 B
SQL
-- Идемпотентность сообщений мессенджера (conversation_messages): клиентский ключ.
|
||
-- Та же защита, что у task_messages (миграция 0086): multi-tab replay
|
||
-- офлайн-очереди, ретраи после сетевых сбоев, двойные клики.
|
||
-- Partial unique index: NULL-значения (старые сообщения, боты) не конфликтуют.
|
||
|
||
ALTER TABLE conversation_messages ADD COLUMN IF NOT EXISTS client_message_id varchar(64);
|
||
|
||
CREATE UNIQUE INDEX IF NOT EXISTS conv_messages_client_message_id_unique
|
||
ON conversation_messages (client_message_id)
|
||
WHERE client_message_id IS NOT NULL;
|