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

@@ -2,7 +2,7 @@ import { users, forms, tasks, taskMessages, messageReads, bots, roles, taskViews
import { taskRelations } from "@shared/schema";
import { taskAssignees, type TaskAssignee } from "@shared/schema";
import { db } from "../db";
import { eq, and, desc, exists, sql, gt } from "drizzle-orm";
import { eq, and, desc, exists, sql, gt, inArray } from "drizzle-orm";
import { formatUserName } from "../utils/formatUserName";
import { TasksCoreStorage } from "./tasks-core.storage";
import { fieldConditionsMatch, resolveFieldDisplayText } from "../utils/field-conditions";
@@ -398,6 +398,32 @@ export class TasksStorage extends TasksCoreStorage {
};
}
/** Батч-загрузка сообщений по списку id одним IN-запросом (сырые строки, tenant-фильтр) */
async getTaskMessagesByIds(messageIds: number[], organizationId: number): Promise<TaskMessage[]> {
if (messageIds.length === 0) return [];
return await db
.select()
.from(taskMessages)
.innerJoin(tasks, eq(taskMessages.taskId, tasks.id))
.innerJoin(forms, eq(tasks.formId, forms.id))
.where(and(inArray(taskMessages.id, messageIds), eq(forms.organizationId, organizationId)))
.orderBy(taskMessages.createdAt)
.then(results => results.map(r => r.task_messages));
}
/** Батч-загрузка сообщений для списка задач одним IN-запросом (сырые строки, tenant-фильтр) */
async getTaskMessagesByTaskIds(taskIds: number[], organizationId: number): Promise<TaskMessage[]> {
if (taskIds.length === 0) return [];
return await db
.select()
.from(taskMessages)
.innerJoin(tasks, eq(taskMessages.taskId, tasks.id))
.innerJoin(forms, eq(tasks.formId, forms.id))
.where(and(inArray(taskMessages.taskId, taskIds), eq(forms.organizationId, organizationId)))
.orderBy(taskMessages.createdAt)
.then(results => results.map(r => r.task_messages));
}
async createTaskMessage(insertMessage: any, organizationId: number): Promise<any> {
const taskExists = await db
.select({ id: tasks.id })