refactor(mcp): структурные инварианты конкурентности записи — resolve-then-lock в хелперах, UUID-assert в page-lock, guard на no-await-окно, политика multi-replica #449
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Зачем
Механика конкурентной записи (write path:
withPageLock→acquireCollabSession→session.mutate) написана аккуратно, но её корректность держится на цепочке конвенций, закреплённых комментариями, а не кодом. Новый write-метод, написанный без знания этих правил, компилируется, проходит большинство тестов — и гоняется в проде. История класса: #260 (slugId/UUID split), #152 (cursor yanking), #159 (split-brain title/body), свежий fail-fast guard0d4f719f.Инварианты-по-конвенции (as-is)
resolvePageId(pageId)доwithPageLock, чтобы и ключ мьютекса, и ключ кэша CollabSession были каноническим UUID (client.ts:~1853, комменты «#260»). ХелперыmutatePage/replacePageпередаютpageIdнасквозь и полностью полагаются на вызывающего. Забыл resolve / передал slugId → лок под другим ключом → тихая потеря сериализации.session.mutateдержится на «NOawaitbetweenTiptapTransformer.fromYdocandapplyDocToFragment» (INVARIANT 1,collab-session.ts:~298-320) — только комментарий; случайныйawaitтихо переоткрывает клоббер живых правок человека.acquireCollabSessionнамеренно не лочит (collab-session.ts:~571); одиночныйinflightReject-guard корректен только пока вызывающий держит page-lock.Map(page-lock.ts:11) — при multi-replica HTTP-деплое без sticky-sessions две реплики держат «лок» одновременно. Та же история у RAM-onlystash_page.MCP_COLLAB_SESSION_MAX_AGE_MS(10 мин) — отозванный доступ лагает на это окно.Механика
mutatePage/replacePage(и родственные хелперы) сами вызываютresolvePageId— убрать обязанность вызывающего;withPageLockассертит UUID-формат ключа (fail-fast на slugId).MCP_COLLAB_SESSION_MAX_AGE_MSв.env.example; опционально — инвалидация кэша сессий при смене прав.Критерии приёмки
awaitв критическое окноmutate→ красный тест/линт.Связано: #416 (роадмап), #435/#439 (кэш collab-токена), #260, #152, #159; коммиты
0d4f719f,572f0a2a.