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:
@@ -115,19 +115,19 @@ export function registerSyncRoutes(app: import("express").Express): void {
|
||||
formIds = Array.from(ids);
|
||||
}
|
||||
|
||||
// Проверяем доступ пользователя к каждой форме
|
||||
const accessibleFormIds: number[] = [];
|
||||
for (const formId of formIds) {
|
||||
const canAccess = await storage.canUserAccessForm(userId, formId, organizationId, 'participate');
|
||||
if (canAccess) accessibleFormIds.push(formId);
|
||||
}
|
||||
// Проверяем доступ пользователя к каждой форме (параллельно, порядок сохраняется)
|
||||
const accessFlags = await Promise.all(
|
||||
formIds.map(formId => storage.canUserAccessForm(userId, formId, organizationId, 'participate'))
|
||||
);
|
||||
const accessibleFormIds = formIds.filter((_, i) => accessFlags[i]);
|
||||
|
||||
const forms: any[] = [];
|
||||
const tasks: any[] = [];
|
||||
const taskFieldValues: any[] = [];
|
||||
const tableIds = new Set<number>();
|
||||
|
||||
for (const formId of accessibleFormIds) {
|
||||
// Загружаем формы и их задачи параллельно; слияние результатов — в исходном порядке formIds
|
||||
const perFormResults = await Promise.all(accessibleFormIds.map(async (formId) => {
|
||||
const [form, formFields, formStatuses, formTabs, transitions] = await Promise.all([
|
||||
storage.getForm(formId, organizationId),
|
||||
storage.getFormFields(formId, organizationId),
|
||||
@@ -136,7 +136,24 @@ export function registerSyncRoutes(app: import("express").Express): void {
|
||||
storage.getStatusTransitions(formId, organizationId),
|
||||
]);
|
||||
|
||||
if (!form) continue;
|
||||
if (!form) return null;
|
||||
|
||||
// Honor the form's offline-cache strategy: 'assigned' fetches only tasks
|
||||
// assigned to the current user; 'recent' / 'all' fetch the latest/up to limit.
|
||||
const offlineCache = (form as any)?.offlineCache as { enabled?: boolean; strategy?: string; maxTasks?: number } | null | undefined;
|
||||
const taskLimit = offlineCache?.maxTasks ?? OFFLINE_TASK_LIMIT;
|
||||
const taskOptions: { limit: number; assignedTo?: number } = { limit: taskLimit };
|
||||
if (offlineCache?.strategy === 'assigned') {
|
||||
taskOptions.assignedTo = userId;
|
||||
}
|
||||
const { tasks: formTasks } = await storage.getTasksWithFieldsByFormOptimized(formId, organizationId, taskOptions);
|
||||
|
||||
return { form, formFields, formStatuses, formTabs, transitions, formTasks };
|
||||
}));
|
||||
|
||||
for (const result of perFormResults) {
|
||||
if (!result) continue;
|
||||
const { form, formFields, formStatuses, formTabs, transitions, formTasks } = result;
|
||||
|
||||
// Собираем ID справочников из полей типа table / select
|
||||
for (const field of formFields) {
|
||||
@@ -163,16 +180,6 @@ export function registerSyncRoutes(app: import("express").Express): void {
|
||||
transitions,
|
||||
});
|
||||
|
||||
// Honor the form's offline-cache strategy: 'assigned' fetches only tasks
|
||||
// assigned to the current user; 'recent' / 'all' fetch the latest/up to limit.
|
||||
const offlineCache = (form as any)?.offlineCache as { enabled?: boolean; strategy?: string; maxTasks?: number } | null | undefined;
|
||||
const taskLimit = offlineCache?.maxTasks ?? OFFLINE_TASK_LIMIT;
|
||||
const taskOptions: { limit: number; assignedTo?: number } = { limit: taskLimit };
|
||||
if (offlineCache?.strategy === 'assigned') {
|
||||
taskOptions.assignedTo = userId;
|
||||
}
|
||||
const { tasks: formTasks } = await storage.getTasksWithFieldsByFormOptimized(formId, organizationId, taskOptions);
|
||||
|
||||
for (const task of formTasks as any[]) {
|
||||
const fieldValues = task.fieldValues || {};
|
||||
delete task.fieldValues;
|
||||
@@ -183,13 +190,12 @@ export function registerSyncRoutes(app: import("express").Express): void {
|
||||
}
|
||||
}
|
||||
|
||||
// Загружаем все доступные справочники организации
|
||||
// Загружаем все доступные справочники организации (строки — параллельно)
|
||||
const dataTables = await storage.getDataTablesByOrganization(organizationId);
|
||||
const dataTableRows: any[] = [];
|
||||
for (const table of dataTables) {
|
||||
const rows = await storage.getDataTableRows(table.id, organizationId);
|
||||
dataTableRows.push(...rows);
|
||||
}
|
||||
const rowsPerTable = await Promise.all(
|
||||
dataTables.map(table => storage.getDataTableRows(table.id, organizationId))
|
||||
);
|
||||
const dataTableRows: any[] = rowsPerTable.flat();
|
||||
|
||||
const automations = await storage.getOfflineAutomations(organizationId);
|
||||
const users = await storage.getUsersByOrganization(organizationId);
|
||||
@@ -248,28 +254,30 @@ export function registerSyncRoutes(app: import("express").Express): void {
|
||||
formIds = Array.from(ids);
|
||||
}
|
||||
|
||||
const accessibleFormIds: number[] = [];
|
||||
for (const formId of formIds) {
|
||||
const canAccess = await storage.canUserAccessForm(userId, formId, organizationId, 'participate');
|
||||
if (canAccess) accessibleFormIds.push(formId);
|
||||
}
|
||||
// Проверяем доступ пользователя к каждой форме (параллельно, порядок сохраняется)
|
||||
const accessFlags = await Promise.all(
|
||||
formIds.map(formId => storage.canUserAccessForm(userId, formId, organizationId, 'participate'))
|
||||
);
|
||||
const accessibleFormIds = formIds.filter((_, i) => accessFlags[i]);
|
||||
|
||||
const changedForms: any[] = [];
|
||||
const changedTasks: any[] = [];
|
||||
const changedFieldValues: any[] = [];
|
||||
const deletedTaskIds: number[] = [];
|
||||
|
||||
for (const formId of accessibleFormIds) {
|
||||
// Дельта по формам — параллельно; слияние результатов — в исходном порядке formIds
|
||||
const perFormResults = await Promise.all(accessibleFormIds.map(async (formId) => {
|
||||
const form = await storage.getForm(formId, organizationId);
|
||||
if (form && (!form.updatedAt || new Date(form.updatedAt) >= since)) {
|
||||
const [fields, statuses, tabs, transitions] = await Promise.all([
|
||||
storage.getFormFields(formId, organizationId),
|
||||
storage.getFormStatuses(formId, organizationId),
|
||||
storage.getFormTabs(formId, organizationId),
|
||||
storage.getStatusTransitions(formId, organizationId),
|
||||
]);
|
||||
changedForms.push({ ...form, fields, statuses, tabs, transitions });
|
||||
}
|
||||
const formChanged = form && (!form.updatedAt || new Date(form.updatedAt) >= since);
|
||||
|
||||
const meta = formChanged
|
||||
? await Promise.all([
|
||||
storage.getFormFields(formId, organizationId),
|
||||
storage.getFormStatuses(formId, organizationId),
|
||||
storage.getFormTabs(formId, organizationId),
|
||||
storage.getStatusTransitions(formId, organizationId),
|
||||
])
|
||||
: null;
|
||||
|
||||
// Honor the form's offline-cache strategy for delta sync as well.
|
||||
const offlineCache = (form as any)?.offlineCache as { enabled?: boolean; strategy?: string; maxTasks?: number } | null | undefined;
|
||||
@@ -279,6 +287,17 @@ export function registerSyncRoutes(app: import("express").Express): void {
|
||||
}
|
||||
// Фильтр по updated_at — на уровне SQL (раньше грузились ВСЕ задачи формы + JS-фильтр)
|
||||
const { tasks: formTasks } = await storage.getTasksWithFieldsByFormOptimized(formId, organizationId, taskOptions);
|
||||
|
||||
return { form, formChanged: !!formChanged, meta, formTasks };
|
||||
}));
|
||||
|
||||
for (const result of perFormResults) {
|
||||
const { form, formChanged, meta, formTasks } = result;
|
||||
if (formChanged && meta) {
|
||||
const [fields, statuses, tabs, transitions] = meta;
|
||||
changedForms.push({ ...form, fields, statuses, tabs, transitions });
|
||||
}
|
||||
|
||||
for (const task of formTasks as any[]) {
|
||||
const fieldValues = task.fieldValues || {};
|
||||
delete task.fieldValues;
|
||||
|
||||
Reference in New Issue
Block a user