fix: cache JSON concorrente, validação do sync e timeout nas chamadas ao Open Food Facts

- JsonCacheManager: a leitura inicial concorrente carregava o arquivo duas
  vezes e a segunda descartava as alterações da primeira; gravações
  simultâneas escreviam no mesmo arquivo. Agora a leitura é compartilhada
  e a gravação é enfileirada, via arquivo temporário + rename.
- POST /api/cache/sync: aceitava qualquer objeto e usava code como chave
  do cache (inclusive ausente ou não numérico); agora só grava produtos
  com code numérico e conta os demais em failed. JSON malformado responde
  400 em vez de 500.
- GET /api/products/[barcode]: resposta da API sem product virava 200
  com corpo vazio; agora responde 404.
- Chamadas ao Open Food Facts (busca e produto) passam a ter timeout de
  15 s e o User-Agent vem de um único lugar (lib/offSearch).
This commit is contained in:
evandro committed 2026-10-10 04:18:59 -03:00
1 parent 5ee5dcd893
commit 30d7ef701f
7 files changed
+273 -30

No files matched your search

+33 -17
View File
@@ -18,6 +18,10 @@ interface JsonCacheData {
export class JsonCacheManager {
private cacheFilePath: string;
private cacheData: JsonCacheData | null = null;
/** Fila de gravações: evita que duas gravações simultâneas corrompam o arquivo. */
private writeQueue: Promise<void> = Promise.resolve();
/** Leitura inicial em andamento, compartilhada por chamadas concorrentes. */
private loadPromise: Promise<void> | null = null;
private static instance: JsonCacheManager;
constructor() {
@@ -53,7 +57,14 @@ export class JsonCacheManager {
/**
* Carrega dados do cache do arquivo JSON
*/
private async loadCache(): Promise<void> {
private loadCache(): Promise<void> {
// Sem isto, duas chamadas simultâneas na primeira leitura carregam o arquivo
// duas vezes e a segunda descarta as alterações feitas pela primeira.
this.loadPromise ??= this.readCacheFile();
return this.loadPromise;
}
private async readCacheFile(): Promise<void> {
if (this.cacheData) return;
try {
@@ -86,23 +97,28 @@ export class JsonCacheManager {
/**
* Salva dados do cache no arquivo JSON
*/
private async saveCache(): Promise<void> {
if (!this.cacheData) return;
private saveCache(): Promise<void> {
const write = async () => {
if (!this.cacheData) return;
try {
await fs.writeFile(
this.cacheFilePath,
JSON.stringify(this.cacheData, null, 2),
"utf-8"
);
console.log(
`[JSON Cache] Cache salvo: ${
Object.keys(this.cacheData.products).length
} produtos`
);
} catch (error) {
console.error("[JSON Cache] Erro ao salvar cache:", error);
}
try {
// Grava em arquivo temporário e renomeia: o arquivo final nunca fica pela metade
const tempPath = `${this.cacheFilePath}.tmp`;
await fs.writeFile(tempPath, JSON.stringify(this.cacheData, null, 2), "utf-8");
await fs.rename(tempPath, this.cacheFilePath);
console.log(
`[JSON Cache] Cache salvo: ${
Object.keys(this.cacheData.products).length
} produtos`
);
} catch (error) {
console.error("[JSON Cache] Erro ao salvar cache:", error);
}
};
// write() nunca rejeita, então a fila continua mesmo após uma falha
this.writeQueue = this.writeQueue.then(write);
return this.writeQueue;
}
/**