Optimize Convex queries and stack config

This commit is contained in:
Esdras Renan 2025-11-12 22:13:50 -03:00
parent 004f345d92
commit 3e4943f79c
4 changed files with 469 additions and 408 deletions

View file

@ -3,7 +3,7 @@ import { mutation, query } from "./_generated/server";
import { api } from "./_generated/api";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { ConvexError, v } from "convex/values";
import { Id, type Doc } from "./_generated/dataModel";
import { Id, type Doc, type TableNames } from "./_generated/dataModel";
import { requireAdmin, requireStaff, requireUser } from "./rbac";
import {
@ -1379,12 +1379,12 @@ function getCustomFieldRecordEntry(
return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
}
const DEFAULT_TICKETS_LIST_LIMIT = 250;
const DEFAULT_TICKETS_LIST_LIMIT = 120;
const MIN_TICKETS_LIST_LIMIT = 25;
const MAX_TICKETS_LIST_LIMIT = 600;
const MAX_FETCH_LIMIT = 1000;
const FETCH_MULTIPLIER_NO_SEARCH = 3;
const FETCH_MULTIPLIER_WITH_SEARCH = 5;
const MAX_TICKETS_LIST_LIMIT = 400;
const MAX_FETCH_LIMIT = 400;
const BASE_FETCH_PADDING = 50;
const SEARCH_FETCH_PADDING = 200;
function clampTicketLimit(limit: number) {
if (!Number.isFinite(limit)) return DEFAULT_TICKETS_LIST_LIMIT;
@ -1392,11 +1392,31 @@ function clampTicketLimit(limit: number) {
}
function computeFetchLimit(limit: number, hasSearch: boolean) {
const multiplier = hasSearch ? FETCH_MULTIPLIER_WITH_SEARCH : FETCH_MULTIPLIER_NO_SEARCH;
const target = limit * multiplier;
const padding = hasSearch ? SEARCH_FETCH_PADDING : BASE_FETCH_PADDING;
const target = limit + padding;
return Math.max(limit, Math.min(MAX_FETCH_LIMIT, target));
}
async function loadDocs<TableName extends TableNames>(
ctx: QueryCtx,
ids: (Id<TableName> | null | undefined)[],
): Promise<Map<string, Doc<TableName>>> {
const uniqueIds = Array.from(
new Set(ids.filter((value): value is Id<TableName> => Boolean(value))),
);
if (uniqueIds.length === 0) {
return new Map();
}
const docs = await Promise.all(uniqueIds.map((id) => ctx.db.get(id)));
const map = new Map<string, Doc<TableName>>();
docs.forEach((doc, index) => {
if (doc) {
map.set(String(uniqueIds[index]), doc);
}
});
return map;
}
function dedupeTicketsById(tickets: Doc<"tickets">[]) {
const seen = new Set<string>();
const result: Doc<"tickets">[] = [];
@ -1515,151 +1535,175 @@ export const list = query({
}
const limited = filtered.slice(0, requestedLimit);
const categoryCache = new Map<string, Doc<"ticketCategories"> | null>();
const subcategoryCache = new Map<string, Doc<"ticketSubcategories"> | null>();
const machineCache = new Map<string, Doc<"machines"> | null>();
// hydrate requester and assignee
const result = await Promise.all(
limited.map(async (t) => {
const requester = (await ctx.db.get(t.requesterId)) as Doc<"users"> | null;
const assignee = t.assigneeId ? ((await ctx.db.get(t.assigneeId)) as Doc<"users"> | null) : null;
const queue = t.queueId ? ((await ctx.db.get(t.queueId)) as Doc<"queues"> | null) : null;
const company = t.companyId ? ((await ctx.db.get(t.companyId)) as Doc<"companies"> | null) : null;
const queueName = normalizeQueueName(queue);
const activeSession = t.activeSessionId ? await ctx.db.get(t.activeSessionId) : null;
let categorySummary: { id: Id<"ticketCategories">; name: string } | null = null;
let subcategorySummary: { id: Id<"ticketSubcategories">; name: string } | null = null;
if (t.categoryId) {
if (!categoryCache.has(t.categoryId)) {
categoryCache.set(t.categoryId, await ctx.db.get(t.categoryId));
}
const category = categoryCache.get(t.categoryId);
if (category) {
categorySummary = { id: category._id, name: category.name };
}
}
if (t.subcategoryId) {
if (!subcategoryCache.has(t.subcategoryId)) {
subcategoryCache.set(t.subcategoryId, await ctx.db.get(t.subcategoryId));
}
const subcategory = subcategoryCache.get(t.subcategoryId);
if (subcategory) {
subcategorySummary = { id: subcategory._id, name: subcategory.name };
}
}
const machineSnapshot = t.machineSnapshot as
| {
hostname?: string
persona?: string
assignedUserName?: string
assignedUserEmail?: string
status?: string
if (limited.length === 0) {
return [];
}
const [
requesterDocs,
assigneeDocs,
queueDocs,
companyDocs,
machineDocs,
activeSessionDocs,
categoryDocs,
subcategoryDocs,
] = await Promise.all([
loadDocs(ctx, limited.map((t) => t.requesterId)),
loadDocs(ctx, limited.map((t) => (t.assigneeId as Id<"users"> | null) ?? null)),
loadDocs(ctx, limited.map((t) => (t.queueId as Id<"queues"> | null) ?? null)),
loadDocs(ctx, limited.map((t) => (t.companyId as Id<"companies"> | null) ?? null)),
loadDocs(ctx, limited.map((t) => (t.machineId as Id<"machines"> | null) ?? null)),
loadDocs(ctx, limited.map((t) => (t.activeSessionId as Id<"ticketWorkSessions"> | null) ?? null)),
loadDocs(ctx, limited.map((t) => (t.categoryId as Id<"ticketCategories"> | null) ?? null)),
loadDocs(ctx, limited.map((t) => (t.subcategoryId as Id<"ticketSubcategories"> | null) ?? null)),
]);
const serverNow = Date.now();
const result = limited.map((t) => {
const requesterSnapshot = t.requesterSnapshot as UserSnapshot | undefined;
const requesterDoc = requesterDocs.get(String(t.requesterId)) ?? null;
const requesterSummary = requesterDoc
? buildRequesterSummary(requesterDoc, t.requesterId, { ticketId: t._id })
: buildRequesterFromSnapshot(t.requesterId, requesterSnapshot, { ticketId: t._id });
const assigneeDoc = t.assigneeId
? assigneeDocs.get(String(t.assigneeId)) ?? null
: null;
const assigneeSummary = t.assigneeId
? assigneeDoc
? {
id: assigneeDoc._id,
name: assigneeDoc.name,
email: assigneeDoc.email,
avatarUrl: assigneeDoc.avatarUrl,
teams: normalizeTeams(assigneeDoc.teams),
}
| undefined;
let machineSummary:
| {
id: Id<"machines"> | null
hostname: string | null
persona: string | null
assignedUserName: string | null
assignedUserEmail: string | null
status: string | null
}
| null = null;
if (t.machineId) {
const cacheKey = String(t.machineId);
if (!machineCache.has(cacheKey)) {
machineCache.set(cacheKey, (await ctx.db.get(t.machineId)) as Doc<"machines"> | null);
: buildAssigneeFromSnapshot(t.assigneeId, t.assigneeSnapshot ?? undefined)
: null;
const queueDoc = t.queueId ? queueDocs.get(String(t.queueId)) ?? null : null;
const queueName = normalizeQueueName(queueDoc);
const companyDoc = t.companyId ? companyDocs.get(String(t.companyId)) ?? null : null;
const companySummary = companyDoc
? { id: companyDoc._id, name: companyDoc.name, isAvulso: companyDoc.isAvulso ?? false }
: t.companyId || t.companySnapshot
? buildCompanyFromSnapshot(t.companyId as Id<"companies"> | undefined, t.companySnapshot ?? undefined)
: null;
const machineSnapshot = t.machineSnapshot as
| {
hostname?: string;
persona?: string;
assignedUserName?: string;
assignedUserEmail?: string;
status?: string;
}
const machineDoc = machineCache.get(cacheKey);
machineSummary = {
id: t.machineId,
hostname: machineDoc?.hostname ?? machineSnapshot?.hostname ?? null,
persona: machineDoc?.persona ?? machineSnapshot?.persona ?? null,
assignedUserName: machineDoc?.assignedUserName ?? machineSnapshot?.assignedUserName ?? null,
assignedUserEmail: machineDoc?.assignedUserEmail ?? machineSnapshot?.assignedUserEmail ?? null,
status: machineDoc?.status ?? machineSnapshot?.status ?? null,
};
} else if (machineSnapshot) {
machineSummary = {
id: null,
hostname: machineSnapshot.hostname ?? null,
persona: machineSnapshot.persona ?? null,
assignedUserName: machineSnapshot.assignedUserName ?? null,
assignedUserEmail: machineSnapshot.assignedUserEmail ?? null,
status: machineSnapshot.status ?? null,
};
}
const serverNow = Date.now()
return {
id: t._id,
reference: t.reference,
tenantId: t.tenantId,
subject: t.subject,
summary: t.summary,
status: normalizeStatus(t.status),
priority: t.priority,
channel: t.channel,
queue: queueName,
csatScore: typeof t.csatScore === "number" ? t.csatScore : null,
csatMaxScore: typeof t.csatMaxScore === "number" ? t.csatMaxScore : null,
csatComment: typeof t.csatComment === "string" && t.csatComment.trim().length > 0 ? t.csatComment.trim() : null,
csatRatedAt: t.csatRatedAt ?? null,
csatRatedBy: t.csatRatedBy ? String(t.csatRatedBy) : null,
formTemplate: t.formTemplate ?? null,
formTemplateLabel: resolveFormTemplateLabel(t.formTemplate ?? null, t.formTemplateLabel ?? null),
company: company
? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false }
: t.companyId || t.companySnapshot
? buildCompanyFromSnapshot(t.companyId as Id<"companies"> | undefined, t.companySnapshot ?? undefined)
: null,
requester: requester
? buildRequesterSummary(requester, t.requesterId, { ticketId: t._id })
: buildRequesterFromSnapshot(
t.requesterId,
t.requesterSnapshot ?? undefined,
{ ticketId: t._id }
),
assignee: t.assigneeId
? assignee
? {
id: assignee._id,
name: assignee.name,
email: assignee.email,
avatarUrl: assignee.avatarUrl,
teams: normalizeTeams(assignee.teams),
}
: buildAssigneeFromSnapshot(t.assigneeId, t.assigneeSnapshot ?? undefined)
: null,
slaPolicy: null,
dueAt: t.dueAt ?? null,
firstResponseAt: t.firstResponseAt ?? null,
resolvedAt: t.resolvedAt ?? null,
updatedAt: t.updatedAt,
createdAt: t.createdAt,
tags: t.tags ?? [],
lastTimelineEntry: null,
metrics: null,
category: categorySummary,
subcategory: subcategorySummary,
machine: machineSummary,
workSummary: {
totalWorkedMs: t.totalWorkedMs ?? 0,
internalWorkedMs: t.internalWorkedMs ?? 0,
externalWorkedMs: t.externalWorkedMs ?? 0,
serverNow,
activeSession: activeSession
? {
id: activeSession._id,
agentId: activeSession.agentId,
startedAt: activeSession.startedAt,
workType: activeSession.workType ?? "INTERNAL",
}
: null,
},
| undefined;
const machineDoc = t.machineId ? machineDocs.get(String(t.machineId)) ?? null : null;
let machineSummary:
| {
id: Id<"machines"> | null;
hostname: string | null;
persona: string | null;
assignedUserName: string | null;
assignedUserEmail: string | null;
status: string | null;
}
| null = null;
if (t.machineId) {
machineSummary = {
id: t.machineId,
hostname: machineDoc?.hostname ?? machineSnapshot?.hostname ?? null,
persona: machineDoc?.persona ?? machineSnapshot?.persona ?? null,
assignedUserName: machineDoc?.assignedUserName ?? machineSnapshot?.assignedUserName ?? null,
assignedUserEmail: machineDoc?.assignedUserEmail ?? machineSnapshot?.assignedUserEmail ?? null,
status: machineDoc?.status ?? machineSnapshot?.status ?? null,
};
})
);
} else if (machineSnapshot) {
machineSummary = {
id: null,
hostname: machineSnapshot.hostname ?? null,
persona: machineSnapshot.persona ?? null,
assignedUserName: machineSnapshot.assignedUserName ?? null,
assignedUserEmail: machineSnapshot.assignedUserEmail ?? null,
status: machineSnapshot.status ?? null,
};
}
const categoryDoc = t.categoryId ? categoryDocs.get(String(t.categoryId)) ?? null : null;
const categorySummary = categoryDoc
? { id: categoryDoc._id, name: categoryDoc.name }
: null;
const subcategoryDoc = t.subcategoryId
? subcategoryDocs.get(String(t.subcategoryId)) ?? null
: null;
const subcategorySummary = subcategoryDoc
? { id: subcategoryDoc._id, name: subcategoryDoc.name, categoryId: subcategoryDoc.categoryId }
: null;
const activeSessionDoc = t.activeSessionId
? activeSessionDocs.get(String(t.activeSessionId)) ?? null
: null;
const activeSession = activeSessionDoc
? {
id: activeSessionDoc._id,
agentId: activeSessionDoc.agentId,
startedAt: activeSessionDoc.startedAt,
workType: activeSessionDoc.workType ?? "INTERNAL",
}
: null;
return {
id: t._id,
reference: t.reference,
tenantId: t.tenantId,
subject: t.subject,
summary: t.summary,
status: normalizeStatus(t.status),
priority: t.priority,
channel: t.channel,
queue: queueName,
csatScore: typeof t.csatScore === "number" ? t.csatScore : null,
csatMaxScore: typeof t.csatMaxScore === "number" ? t.csatMaxScore : null,
csatComment:
typeof t.csatComment === "string" && t.csatComment.trim().length > 0
? t.csatComment.trim()
: null,
csatRatedAt: t.csatRatedAt ?? null,
csatRatedBy: t.csatRatedBy ? String(t.csatRatedBy) : null,
formTemplate: t.formTemplate ?? null,
formTemplateLabel: resolveFormTemplateLabel(
t.formTemplate ?? null,
t.formTemplateLabel ?? null,
),
company: companySummary,
requester: requesterSummary,
assignee: assigneeSummary,
slaPolicy: null,
dueAt: t.dueAt ?? null,
firstResponseAt: t.firstResponseAt ?? null,
resolvedAt: t.resolvedAt ?? null,
updatedAt: t.updatedAt,
createdAt: t.createdAt,
tags: t.tags ?? [],
lastTimelineEntry: null,
metrics: null,
category: categorySummary,
subcategory: subcategorySummary,
machine: machineSummary,
workSummary: {
totalWorkedMs: t.totalWorkedMs ?? 0,
internalWorkedMs: t.internalWorkedMs ?? 0,
externalWorkedMs: t.externalWorkedMs ?? 0,
serverNow,
activeSession,
},
};
});
// sort by updatedAt desc
return result.sort((a, b) => (b.updatedAt ?? 0) - (a.updatedAt ?? 0));
},