feat: enhance push notification management and error handling
- Implemented error handling for unhandled promise rejections related to message channels, improving stability during push notification operations. - Updated the PushNotificationManager component to manage push subscription registration with timeouts, preventing application hangs. - Enhanced the sidebar and chat components to display user avatars, improving user experience and visual consistency. - Refactored email processing logic to support scheduled email sending, integrating new backend functionalities for better email management. - Improved overall error handling and logging across components to reduce console spam and enhance debugging capabilities.
This commit is contained in:
@@ -2,6 +2,7 @@ import { v } from "convex/values";
|
||||
import { mutation, query, internalMutation, internalQuery, action } from "./_generated/server";
|
||||
import { internal, api } from "./_generated/api";
|
||||
import { renderizarTemplate } from "./templatesMensagens";
|
||||
import type { Doc, Id } from "./_generated/dataModel";
|
||||
|
||||
// ========== INTERNAL QUERIES ==========
|
||||
|
||||
@@ -116,8 +117,14 @@ export const enfileirarEmail = mutation({
|
||||
corpo: v.string(),
|
||||
templateId: v.optional(v.id("templatesMensagens")),
|
||||
enviadoPor: v.id("usuarios"), // Obrigatório conforme schema
|
||||
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Validar agendamento se fornecido
|
||||
if (args.agendadaPara !== undefined && args.agendadaPara <= Date.now()) {
|
||||
throw new Error("Data de agendamento deve ser futura");
|
||||
}
|
||||
|
||||
const emailId = await ctx.db.insert("notificacoesEmail", {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
@@ -128,8 +135,13 @@ export const enfileirarEmail = mutation({
|
||||
tentativas: 0,
|
||||
criadoEm: Date.now(),
|
||||
enviadoPor: args.enviadoPor,
|
||||
agendadaPara: args.agendadaPara,
|
||||
});
|
||||
|
||||
// O cron job processará emails automaticamente:
|
||||
// - Emails sem agendamento serão processados imediatamente (próxima execução do cron)
|
||||
// - Emails agendados serão processados quando a hora chegar
|
||||
|
||||
return emailId;
|
||||
},
|
||||
});
|
||||
@@ -144,12 +156,16 @@ export const enviarEmailComTemplate = action({
|
||||
templateCodigo: v.string(),
|
||||
variaveis: v.optional(v.record(v.string(), v.string())),
|
||||
enviadoPor: v.id("usuarios"), // Obrigatório conforme schema
|
||||
agendadaPara: v.optional(v.number()), // timestamp opcional para agendamento
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
handler: async (ctx, args): Promise<Id<"notificacoesEmail">> => {
|
||||
// Buscar template
|
||||
const template = await ctx.runQuery(api.templatesMensagens.obterTemplatePorCodigo, {
|
||||
codigo: args.templateCodigo,
|
||||
});
|
||||
const template: Doc<"templatesMensagens"> | null = await ctx.runQuery(
|
||||
api.templatesMensagens.obterTemplatePorCodigo,
|
||||
{
|
||||
codigo: args.templateCodigo,
|
||||
}
|
||||
);
|
||||
|
||||
if (!template) {
|
||||
throw new Error(`Template não encontrado: ${args.templateCodigo}`);
|
||||
@@ -160,16 +176,21 @@ export const enviarEmailComTemplate = action({
|
||||
const tituloRenderizado = renderizarTemplate(template.titulo, variaveisTemplate);
|
||||
const corpoRenderizado = renderizarTemplate(template.corpo, variaveisTemplate);
|
||||
|
||||
// Enfileirar email
|
||||
const emailId = await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
// Enfileirar email via mutation
|
||||
const emailId: Id<"notificacoesEmail"> = await ctx.runMutation(api.email.enfileirarEmail, {
|
||||
destinatario: args.destinatario,
|
||||
destinatarioId: args.destinatarioId,
|
||||
assunto: tituloRenderizado,
|
||||
corpo: corpoRenderizado,
|
||||
templateId: template._id,
|
||||
templateId: template._id, // template._id sempre existe se template não é null
|
||||
enviadoPor: args.enviadoPor,
|
||||
agendadaPara: args.agendadaPara,
|
||||
});
|
||||
|
||||
if (!emailId) {
|
||||
throw new Error("Erro ao enfileirar email: ID não retornado");
|
||||
}
|
||||
|
||||
return emailId;
|
||||
},
|
||||
});
|
||||
@@ -182,29 +203,45 @@ export const enviarEmailComTemplate = action({
|
||||
export const processarFilaEmails = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
// Buscar emails pendentes (limitado a 10 por vez para não sobrecarregar)
|
||||
const emailsPendentes = await ctx.db
|
||||
const agora = Date.now();
|
||||
|
||||
// Buscar emails pendentes que devem ser processados agora
|
||||
// (sem agendamento OU com agendamento que já passou)
|
||||
const emailsParaProcessar = await ctx.db
|
||||
.query("notificacoesEmail")
|
||||
.filter((q) => q.eq(q.field("status"), "pendente"))
|
||||
.filter((q) => {
|
||||
const statusPendente = q.eq(q.field("status"), "pendente");
|
||||
const semAgendamento = q.eq(q.field("agendadaPara"), undefined);
|
||||
const agendamentoJaPassou = q.and(
|
||||
q.neq(q.field("agendadaPara"), undefined),
|
||||
q.lte(q.field("agendadaPara"), agora)
|
||||
);
|
||||
|
||||
return q.and(
|
||||
statusPendente,
|
||||
q.or(semAgendamento, agendamentoJaPassou)
|
||||
);
|
||||
})
|
||||
.order("asc") // Mais antigos primeiro
|
||||
.take(10);
|
||||
|
||||
if (emailsPendentes.length === 0) {
|
||||
if (emailsParaProcessar.length === 0) {
|
||||
return { processados: 0 };
|
||||
}
|
||||
|
||||
// Agendar envio de cada email via action
|
||||
for (const email of emailsPendentes) {
|
||||
for (const email of emailsParaProcessar) {
|
||||
// Agendar envio assíncrono (não bloqueia o cron)
|
||||
ctx.scheduler.runAfter(0, api.actions.email.enviar, {
|
||||
emailId: email._id,
|
||||
}).catch((error) => {
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, error);
|
||||
}).catch((error: unknown) => {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
return { processados: emailsPendentes.length };
|
||||
},
|
||||
return { processados: emailsParaProcessar.length };
|
||||
}
|
||||
});
|
||||
|
||||
// ========== QUERIES ==========
|
||||
@@ -221,6 +258,7 @@ export const listarFilaEmails = query({
|
||||
v.literal("enviado"),
|
||||
v.literal("falha")
|
||||
)),
|
||||
_refresh: v.optional(v.number()), // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
let emails;
|
||||
@@ -247,7 +285,9 @@ export const listarFilaEmails = query({
|
||||
* Obter estatísticas da fila de emails (para debug e monitoramento)
|
||||
*/
|
||||
export const obterEstatisticasFilaEmails = query({
|
||||
args: {},
|
||||
args: {
|
||||
_refresh: v.optional(v.number()), // Parâmetro ignorado, usado apenas para forçar refresh no frontend
|
||||
},
|
||||
returns: v.object({
|
||||
pendentes: v.number(),
|
||||
enviando: v.number(),
|
||||
@@ -324,14 +364,15 @@ export const processarFilaEmailsManual = action({
|
||||
emailId: email._id,
|
||||
});
|
||||
processados++;
|
||||
} catch (error) {
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, error);
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
console.error(`Erro ao agendar envio de email ${email._id}:`, errorMessage);
|
||||
falhas++;
|
||||
}
|
||||
}
|
||||
|
||||
return { sucesso: true, processados, falhas };
|
||||
} catch (error) {
|
||||
} catch (error: unknown) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
sucesso: false,
|
||||
|
||||
Reference in New Issue
Block a user