9685074237
Побочные находки инцидента #400 (правка большой таблицы через MCP подвешивает всё). 1. Гонка connect-vs-unload в @hocuspocus/server 3.4.4 (вероятный источник 25s connect-таймаутов): createDocument проверяет loadingDocuments/documents, но НЕ ждёт unloadingDocuments -> новое соединение может захендшейкаться на умирающий Document -> redis-sync идёт по пути 'doc не загружен', провайдер висит до таймаута. Апстрим (main) не починен. pnpm-патч (инфра как у yjs-патча): в начале createDocument await in-flight unload (обёрнут в try/catch — отклонённый unload не отравляет открытие, поведение как до патча), в ОБОИХ рантаймах (cjs+esm). Тест hocuspocus-unload-race: реальный createDocument с засеянным in-flight unload -> не грузит пока unload не осел; при откате патча тест краснеет. 2. Двойная перекодировка в onLoadDocument (persistence.extension.ts): хук строил НОВЫЙ Y.Doc и возвращал его -> hocuspocus делал applyUpdate(encodeStateAsUpdate) ВТОРОЙ раз (315КБ на каждую холодную загрузку); в JSON-ветке результат encode выбрасывался (мёртвый вызов). Теперь стейт применяется прямо в data.document, возврат undefined (hocuspocus мержит только при возврате Doc); мёртвый encode убран. Содержимое документа не меняется — только меньше encode/alloc. 3. isDeepStrictEqual по 84КБ JSON на каждом store: замерил — 1.32мс на 90КБ (immaterial, <50мс порога; доминируют fromYdoc+encodeStateAsUpdate). Изменений кода НЕТ по правилу задачи (dirty-флаг только при material). closes #401 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
4.2 KiB
Diff
63 lines
4.2 KiB
Diff
diff --git a/dist/hocuspocus-server.cjs b/dist/hocuspocus-server.cjs
|
|
index b24ff6d091c32f733089eeaa47b03f7b37cf5964..f003af304fc751b7edc1aee17f3651282d70666a 100644
|
|
--- a/dist/hocuspocus-server.cjs
|
|
+++ b/dist/hocuspocus-server.cjs
|
|
@@ -2426,6 +2426,26 @@ class Hocuspocus {
|
|
* Create a new document by the given request
|
|
*/
|
|
async createDocument(documentName, request, socketId, connection, context) {
|
|
+ // PATCH(gitmost #401): close the connect-vs-unload race. When the last
|
|
+ // client disconnects, storeDocumentHooks' finally schedules an unload;
|
|
+ // unloadDocument runs its beforeUnloadDocument hooks asynchronously and
|
|
+ // records an in-flight promise in `unloadingDocuments`. A NEW connection
|
|
+ // arriving in that window would otherwise fall straight through to the
|
|
+ // `documents`/`loadingDocuments` checks and could either reuse a doc that
|
|
+ // is about to be destroyed, or start loading a fresh doc concurrently
|
|
+ // with the destroy. Awaiting the in-flight unload first makes the decision
|
|
+ // deterministic: once it settles, either the doc was fully unloaded
|
|
+ // (removed from `documents`, so we do a clean fresh load below) or the
|
|
+ // unload aborted because work/connections reappeared (the healthy doc is
|
|
+ // still in `documents`, so we reuse it). Either way the new connection can
|
|
+ // never hand-shake onto an orphaned, about-to-be-destroyed Document.
|
|
+ const existingUnloadingDoc = this.unloadingDocuments.get(documentName);
|
|
+ if (existingUnloadingDoc) {
|
|
+ // Wait for the in-flight unload to settle so we never hand-shake onto a dying
|
|
+ // Document. Swallow a rejected unload — fall through to a fresh load, matching
|
|
+ // pre-patch behavior (the doc is already removed from `documents` by then).
|
|
+ try { await existingUnloadingDoc; } catch { /* unload rejected — fresh load */ }
|
|
+ }
|
|
const existingLoadingDoc = this.loadingDocuments.get(documentName);
|
|
if (existingLoadingDoc) {
|
|
return existingLoadingDoc;
|
|
diff --git a/dist/hocuspocus-server.esm.js b/dist/hocuspocus-server.esm.js
|
|
index 1f4dd80244e899128e2c4e5dad8eab7cfc1cbad6..8c2411747bba27fb9486e1df81678a14e41e884e 100644
|
|
--- a/dist/hocuspocus-server.esm.js
|
|
+++ b/dist/hocuspocus-server.esm.js
|
|
@@ -2406,6 +2406,26 @@ class Hocuspocus {
|
|
* Create a new document by the given request
|
|
*/
|
|
async createDocument(documentName, request, socketId, connection, context) {
|
|
+ // PATCH(gitmost #401): close the connect-vs-unload race. When the last
|
|
+ // client disconnects, storeDocumentHooks' finally schedules an unload;
|
|
+ // unloadDocument runs its beforeUnloadDocument hooks asynchronously and
|
|
+ // records an in-flight promise in `unloadingDocuments`. A NEW connection
|
|
+ // arriving in that window would otherwise fall straight through to the
|
|
+ // `documents`/`loadingDocuments` checks and could either reuse a doc that
|
|
+ // is about to be destroyed, or start loading a fresh doc concurrently
|
|
+ // with the destroy. Awaiting the in-flight unload first makes the decision
|
|
+ // deterministic: once it settles, either the doc was fully unloaded
|
|
+ // (removed from `documents`, so we do a clean fresh load below) or the
|
|
+ // unload aborted because work/connections reappeared (the healthy doc is
|
|
+ // still in `documents`, so we reuse it). Either way the new connection can
|
|
+ // never hand-shake onto an orphaned, about-to-be-destroyed Document.
|
|
+ const existingUnloadingDoc = this.unloadingDocuments.get(documentName);
|
|
+ if (existingUnloadingDoc) {
|
|
+ // Wait for the in-flight unload to settle so we never hand-shake onto a dying
|
|
+ // Document. Swallow a rejected unload — fall through to a fresh load, matching
|
|
+ // pre-patch behavior (the doc is already removed from `documents` by then).
|
|
+ try { await existingUnloadingDoc; } catch { /* unload rejected — fresh load */ }
|
|
+ }
|
|
const existingLoadingDoc = this.loadingDocuments.get(documentName);
|
|
if (existingLoadingDoc) {
|
|
return existingLoadingDoc;
|