perf(sse+tasks): индекс SSE-соединений, getTaskTree на recursive CTE, батчи N+1

Шаги 1.4 и 1.5 плана production-готовности:
- SseConnectionIndex (byOrg/byUser), один heartbeat-таймер, протокол SSE не тронут
- getTaskTree/getTaskParentChain: WITH RECURSIVE CTE (было 1+2N запросов)
- sync initial/delta: Promise.all по формам
- embedding queue/reindex: батч-предзагрузки вместо поштучных запросов
- 8 новых тестов (96/96)
This commit is contained in:
2026-09-07 23:29:21 +03:00
parent 9528a8123a
commit 667a321341
10 changed files with 518 additions and 121 deletions

View File

@@ -4900,14 +4900,11 @@ To block task creation from task.before_create, set: ctx.result = { allow: false
};
};
// Цепочка родителей вверх по parentTaskId (с защитой от циклов)
// Цепочка родителей вверх по parentTaskId (ближайший первым, до 10 уровней,
// один recursive CTE вместо последовательных getTask на каждый уровень)
const parentChain = await storage.getTaskParentChain(taskId, organizationId, 10);
const parents: Array<Record<string, unknown>> = [];
const seen = new Set<number>([task.id]);
let cursor = task.parentTaskId ?? null;
while (cursor !== null && !seen.has(cursor) && parents.length < 10) {
seen.add(cursor);
const parent = await storage.getTask(cursor, organizationId);
if (!parent) break;
for (const parent of parentChain) {
if (isFormAllowed(parent.formId)) {
parents.push({
id: parent.id,
@@ -4917,7 +4914,6 @@ To block task creation from task.before_create, set: ctx.result = { allow: false
isCompleted: parent.isCompleted,
});
}
cursor = parent.parentTaskId ?? null;
}
return {