AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / coding / Perguntas / 79565459
Accepted
Marcus-Flavio
Marcus-Flavio
Asked: 2025-04-10 07:05:51 +0800 CST2025-04-10 07:05:51 +0800 CST 2025-04-10 07:05:51 +0800 CST

A data retorna o mesmo valor em várias solicitações quando implantada no Vercel (Next.js Route Handler)

  • 772

Estou usando uma rota da API do Next.js App Router (app/api/notify/route.ts) para verificar a hora atual (no fuso horário do Brasil) e disparar notificações quando um horário agendado for atingido.

Isso funciona localmente, mas depois de implantar no Vercel, notei um comportamento inesperado:

❌ Problema: Após a primeira solicitação, todas as solicitações subsequentes retornam o mesmo registro de data e hora (tempo congelado desde a primeira execução), mesmo que eu esteja usando new Date() no manipulador.

Parece que a função está sendo armazenada em cache ou o ambiente está reutilizando o mesmo contexto.

✅ Comportamento esperado: toda vez que a rota for atingida, new Date() deve retornar a hora atual, refletindo o momento real da solicitação.

✅ Etapas reproduzíveis: implante este manipulador simples no Vercel.

Envie várias solicitações GET para o endpoint com alguns segundos/minutos de atraso.

Você notará que o timestamp e o isoTime nunca mudam após a primeira chamada.

import connectMongo from "@/libs/mongoose";
import Routine from "@/models/Routine";
import User from "@/models/User";
import PushSubscription from "@/models/PushSubscription";
const webPush = require("web-push");

const vapidPublicKey = process.env.NEXT_PUBLIC_WEB_PUSH_PUBLIC_KEY;
const vapidPrivateKey = process.env.WEB_PUSH_PRIVATE_KEY;

if (!vapidPublicKey || !vapidPrivateKey) {
    throw new Error("VAPID keys not configured");
}

console.log("VAPID keys configured correctly");
webPush.setVapidDetails("mailto:[email protected]", vapidPublicKey, vapidPrivateKey);

const getBrazilDateTime = () => {
    const now = new Date();
    const brazilTime = new Date(now.getTime() - 3 * 60 * 60 * 1000); // adjust for Brazil timezone
    return brazilTime;
};

const getDayNameInPortuguese = (date) => {
    const days = ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado"];
    return days[date.getDay()];
};

const getTaskStartTime = (task, currentDay) => {
    if (task.dailySchedule instanceof Map && task.dailySchedule.has(currentDay)) {
        const daySchedule = task.dailySchedule.get(currentDay);
        if (daySchedule && daySchedule.startTime) return daySchedule.startTime;
    } else if (task.dailySchedule && typeof task.dailySchedule === "object" && task.dailySchedule[currentDay]?.startTime) {
        return task.dailySchedule[currentDay].startTime;
    }
    if (task.startTime) return task.startTime;
    return null;
};

const deduplicateSubscriptions = (subscriptions) => {
    const uniqueEndpoints = new Set();
    return subscriptions.filter((sub) => {
        if (uniqueEndpoints.has(sub.endpoint)) return false;
        uniqueEndpoints.add(sub.endpoint);
        return true;
    });
};

const notifiedTasksCache = new Map();

const isTaskAlreadyNotified = (taskId, userId) => {
    const key = `${taskId}-${userId}`;
    const lastNotified = notifiedTasksCache.get(key);
    if (!lastNotified) return false;
    const tenMinutesAgo = Date.now() - 10 * 60 * 1000;
    return lastNotified > tenMinutesAgo;
};

const markTaskAsNotified = (taskId, userId) => {
    const key = `${taskId}-${userId}`;
    notifiedTasksCache.set(key, Date.now());
};

export async function GET(request) {
    const headers = new Headers({
        "Cache-Control": "no-store, max-age=0, must-revalidate",
        "Content-Type": "application/json",
    });

    const logs = [];
    const addLog = (message) => {
        console.log(message);
        logs.push(`[${new Date().toISOString()}] ${message}`);
    };

    addLog("🔔 Starting notification check...");
    addLog(`🕒 Start timestamp: ${Date.now()}`);

    try {
        addLog("Connecting to MongoDB...");
        await connectMongo();
        addLog("MongoDB connection established");

        // Update the time on each cycle to ensure the time is current.
        const spDate = getBrazilDateTime(); 
        const currentDay = getDayNameInPortuguese(spDate);
        const currentTime = spDate.toLocaleTimeString("pt-BR", {
            hour: "2-digit",
            minute: "2-digit",
            hour12: false,
        });

        addLog(`📅 Brazil date and time: ${spDate.toLocaleString("pt-BR")}`);
        addLog(`📅 Day of the week: ${currentDay}`);
        addLog(`⏰ Current time: ${currentTime}`);

        const users = await User.find({ activeRoutine: { $exists: true, $ne: null } });
        addLog(`👥 Found ${users.length} users with active routines`);

        if (!users.length) return NextResponse.json({ message: "No users with active routines found." });

        const routineIds = users.map((user) => user.activeRoutine).filter(Boolean);
        const routines = await Routine.find({ _id: { $in: routineIds } });
        const routineMap = new Map();
        routines.forEach((routine) => routineMap.set(routine._id.toString(), routine));

        let notificationsSent = 0;
        let usersNotified = 0;
        let duplicatesSkipped = 0;

        await Promise.all(
            users.map(async (user) => {
                const routineId = user.activeRoutine?.toString();
                if (!routineId) return;
                const routine = routineMap.get(routineId);
                if (!routine) return;

                const matchingTasks = routine.tasks.filter((task) => {
                    const taskDays = task.days || [];
                    const includesDay = taskDays.includes(currentDay);
                    const taskStartTime = getTaskStartTime(task, currentDay);
                    return includesDay && taskStartTime === currentTime;
                });

                if (!matchingTasks.length) return;

                for (const matchingTask of matchingTasks) {
                    if (isTaskAlreadyNotified(matchingTask._id.toString(), user._id.toString())) {
                        duplicatesSkipped++;
                        continue;
                    }

                    let subscriptions = await PushSubscription.find({ userId: user._id });
                    if (!subscriptions.length) continue;

                    subscriptions = deduplicateSubscriptions(subscriptions);
                    addLog(`  📱 User ${user.email} has ${subscriptions.length} unique devices`);

                    const payload = JSON.stringify({
                        title: `🔔 ${matchingTask.name} - Time to start!`,
                        body: `⏰ ${currentTime} - ${matchingTask.details || "Stay focused on your routine!"}`,
                        icon: "/icon512_rounded.png",
                        badge: "/icon192_rounded.png",
                        tag: `task-${matchingTask._id}`,
                        data: {
                            url: `/dashboard/r/${routine._id}`,
                            taskId: matchingTask._id.toString(),
                            type: "task-reminder",
                            timestamp: new Date().toISOString(),
                        },
                        actions: [
                            { action: "open", title: "📋 View Details" },
                            { action: "dismiss", title: "✔️ Got it" },
                        ],
                        vibrate: [200, 100, 200],
                        requireInteraction: true,
                    });

                    await Promise.all(
                        subscriptions.map(async (subscription) => {
                            try {
                                await webPush.sendNotification(
                                    {
                                        endpoint: subscription.endpoint,
                                        keys: subscription.keys,
                                    },
                                    payload
                                );
                                notificationsSent++;
                                markTaskAsNotified(matchingTask._id.toString(), user._id.toString());
                            } catch (error) {
                                if (error.statusCode === 410) {
                                    await PushSubscription.deleteOne({ _id: subscription._id });
                                }
                            }
                        })
                    );
                    usersNotified++;
                }
            })
        );

        return NextResponse.json(
            {
                message: `Notifications sent successfully!`,
                notificationsSent,
                usersNotified,
                duplicatesSkipped,
                logs,
            },
            { headers }
        );
    } catch (error) {
        console.error("Error sending notifications:", error);
        return NextResponse.json({ error: "Error processing notifications.", logs }, { status: 500, headers });
    }
}

🔍 O que tentei: removi toda a lógica externa — o manipulador mínimo acima ainda reproduz o problema.

Cabeçalhos de controle de cache Vercel: não parecem relacionados.

Foi verificado se uma variável global ou estática está retendo o estado — não é o caso.

❓ Minha pergunta: Este é um problema conhecido com o cache de funções ou comportamento de inicialização a frio do Vercel no Next.js App Router?

Como posso garantir que new Date() seja avaliado em todas as solicitações, não apenas na primeira?

javascript
  • 2 2 respostas
  • 40 Views

2 respostas

  • Voted
  1. Best Answer
    john edelbi
    2025-04-10T17:44:28+08:002025-04-10T17:44:28+08:00

    O principal problema é a reutilização de instâncias de função no ambiente sem servidor do Vercel. Veja como corrigir:

    import { NextResponse } from "next/server";
    

    Corrija o tratamento de data: O principal problema é que, mesmo que você esteja chamando new Date() dentro do seu manipulador, outras partes do seu código podem ser armazenadas em cache.

     const getBrazilDateTime = () => {
      // Force a fresh Date object on every call
      const now = new Date(); 
      // Convert to Brazil timezone (UTC-3)
      return new Intl.DateTimeFormat('pt-BR', {
        timeZone: 'America/Sao_Paulo',
        year: 'numeric',
        month: 'numeric',
        day: 'numeric',
        hour: 'numeric',
        minute: 'numeric',
        second: 'numeric',
        hour12: false
      }).format(now);
    };
    

    Adicione um sinalizador de recarga forçada para garantir que as funções não sejam reutilizadas:

    export const dynamic = 'force-dynamic';
    export const revalidate = 0;
    

    ou com o tratamento adequado do fuso horário: em vez de ajustar manualmente o horário subtraindo 3 horas, use a API internacional para o tratamento adequado do fuso horário:

    const getBrazilDateTime = () => {
      const now = new Date();
      // Using proper timezone conversion
      const formatter = new Intl.DateTimeFormat('pt-BR', {
        timeZone: 'America/Sao_Paulo',
      });
      return new Date(formatter.format(now));
    };
    
    • 1
  2. darkknight
    2025-04-10T09:34:36+08:002025-04-10T09:34:36+08:00

    A plataforma sem servidor da Vercel é construída no AWS Lambda. A documentação da AWS afirma que

    Quando a função é invocada novamente, o Lambda descongela o ambiente para reutilização . A reutilização do ambiente de execução tem as seguintes implicações:

    • Objetos declarados fora do método manipulador da função permanecem inicializados, proporcionando otimização adicional quando a função é invocada novamente . Por exemplo, se sua função Lambda estabelecer uma conexão com o banco de dados, em vez de restabelecer a conexão, a conexão original será usada em invocações subsequentes. Recomendamos adicionar lógica ao seu código para verificar se existe uma conexão antes de criar uma nova.

    E a documentação da Vercel sobre a função serverless afirma que:

    O Vercel cria uma nova invocação de função para cada solicitação recebida. Se outra solicitação chegar logo após a anterior, o Vercel reutiliza a mesma instância de função para otimizar o desempenho e a eficiência de custos . Com o tempo, o Vercel mantém apenas as funções ativas necessárias para lidar com o seu tráfego. O Vercel reduz suas funções a zero quando não há solicitações recebidas.

    Ao permitir a execução simultânea na mesma instância (e, portanto, usar o tempo ocioso para computação), a computação fluida reduz as inicializações a frio, diminui a latência e economiza em custos de computação.

    Acho que isso explica tudo. Embora sua getBrazilDateTime()função crie corretamente um novo objeto Date cada vez que é chamada, a instância da função sem servidor permanece "quente" entre as solicitações.

    Basicamente, o escopo externo da sua função é avaliado apenas uma vez durante a inicialização da instância. Apenas a própria função do manipulador ( GETno seu caso) é reexecutada a cada solicitação.

    Acredito que chamar Date.now()e construir explicitamente uma nova Data a partir desse carimbo de data/hora dentro da função manipuladora resolverá esse problema. Isso forçará o JavaScript a obter o carimbo de data/hora atual em vez de reutilizar um valor calculado anteriormente.

    export async function GET(request) {
        // your code...
    
        // get fresh time by explicitly passing a UNIX timestamp to Date constructor
        const spDate = new Date(Date.now() - 3 * 60 * 60 * 1000); // Brazil time
    
        // your code...
    }
    

    Você também pode adicionar uma verificação de tempo mais robusta como esta.

    const spDate = new Date(Date.now() - 3 * 60 * 60 * 1000);
    const currentMinute = `${spDate.getHours().toString().padStart(2, '0')}:${spDate.getMinutes().toString().padStart(2, '0')}`;
    
    // Log with more debugging info
    addLog(`📅 Brazil date and time: ${spDate.toLocaleString("pt-BR")}`);
    addLog(`⏰ Current time (HH:MM): ${currentMinute}`);
    addLog(`*** Raw timestamp: ${Date.now()}`);
    
    • 0

relate perguntas

  • classificação de mesclagem não está funcionando - código Javascript: não é possível encontrar o erro mesmo após a depuração

  • método select.remove() funciona estranho [fechado]

  • Sempre um 401 res em useOpenWeather () - react-open-weather lib [duplicado]

  • O elemento de entrada não possui atributo somente leitura, mas os campos ainda não podem ser editados [fechado]

  • Como editar o raio do primeiro nó de um RadialTree D3.js?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Reformatar números, inserindo separadores em posições fixas

    • 6 respostas
  • Marko Smith

    Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não?

    • 2 respostas
  • Marko Smith

    Problema com extensão desinstalada automaticamente do VScode (tema Material)

    • 2 respostas
  • Marko Smith

    Vue 3: Erro na criação "Identificador esperado, mas encontrado 'import'" [duplicado]

    • 1 respostas
  • Marko Smith

    Qual é o propósito de `enum class` com um tipo subjacente especificado, mas sem enumeradores?

    • 1 respostas
  • Marko Smith

    Como faço para corrigir um erro MODULE_NOT_FOUND para um módulo que não importei manualmente?

    • 6 respostas
  • Marko Smith

    `(expression, lvalue) = rvalue` é uma atribuição válida em C ou C++? Por que alguns compiladores aceitam/rejeitam isso?

    • 3 respostas
  • Marko Smith

    Um programa vazio que não faz nada em C++ precisa de um heap de 204 KB, mas não em C

    • 1 respostas
  • Marko Smith

    PowerBI atualmente quebrado com BigQuery: problema de driver Simba com atualização do Windows

    • 2 respostas
  • Marko Smith

    AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos

    • 1 respostas
  • Martin Hope
    Fantastic Mr Fox Somente o tipo copiável não é aceito na implementação std::vector do MSVC 2025-04-23 06:40:49 +0800 CST
  • Martin Hope
    Howard Hinnant Encontre o próximo dia da semana usando o cronógrafo 2025-04-21 08:30:25 +0800 CST
  • Martin Hope
    Fedor O inicializador de membro do construtor pode incluir a inicialização de outro membro? 2025-04-15 01:01:44 +0800 CST
  • Martin Hope
    Petr Filipský Por que os conceitos do C++20 causam erros de restrição cíclica, enquanto o SFINAE antigo não? 2025-03-23 21:39:40 +0800 CST
  • Martin Hope
    Catskul O C++20 mudou para permitir a conversão de `type(&)[N]` de matriz de limites conhecidos para `type(&)[]` de matriz de limites desconhecidos? 2025-03-04 06:57:53 +0800 CST
  • Martin Hope
    Stefan Pochmann Como/por que {2,3,10} e {x,3,10} com x=2 são ordenados de forma diferente? 2025-01-13 23:24:07 +0800 CST
  • Martin Hope
    Chad Feller O ponto e vírgula agora é opcional em condicionais bash com [[ .. ]] na versão 5.2? 2024-10-21 05:50:33 +0800 CST
  • Martin Hope
    Wrench Por que um traço duplo (--) faz com que esta cláusula MariaDB seja avaliada como verdadeira? 2024-05-05 13:37:20 +0800 CST
  • Martin Hope
    Waket Zheng Por que `dict(id=1, **{'id': 2})` às vezes gera `KeyError: 'id'` em vez de um TypeError? 2024-05-04 14:19:19 +0800 CST
  • Martin Hope
    user924 AdMob: MobileAds.initialize() - "java.lang.Integer não pode ser convertido em java.lang.String" para alguns dispositivos 2024-03-20 03:12:31 +0800 CST

Hot tag

python javascript c++ c# java typescript sql reactjs html

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve