Desafio JavaScript

Cache com WeakMap e contagem de acessos

Enunciado

Implemente uma função criarCache() que retorna um objeto com dois métodos: obter(chave, calcular) e estatisticas(). O método obter recebe um objeto chave e uma função calcular. Se o resultado para aquela chave ainda não estiver no cache, ele chama calcular(chave), armazena o resultado em um WeakMap e retorna. Se já estiver, retorna o valor armazenado. O método estatisticas retorna um Map com a contagem de quantas vezes cada chave foi acessada (incluindo acertos e erros de cache).

Requisitos

  • Use WeakMap para armazenar os resultados do cache.
  • Use Map para armazenar as contagens de acesso por chave.
  • O método obter deve chamar calcular apenas na primeira vez para cada chave.
  • O método estatisticas deve retornar um Map com as contagens.
  • As chaves são objetos e não devem vazar memória (WeakMap).

Código inicial

function criarCache() {
  // seu código aqui
}

// Exemplo de uso:
const cache = criarCache();
const obj1 = {};
const obj2 = {};

console.log(cache.obter(obj1, () => 'valor1')); // 'valor1'
console.log(cache.obter(obj1, () => 'valor1')); // 'valor1' (do cache)
console.log(cache.obter(obj2, () => 'valor2')); // 'valor2'
console.log(cache.estatisticas()); // Map(2) { {} => 2, {} => 1 }

Saída esperada

valor1
valor1
valor2
Map(2) { {} => 2, {} => 1 }
Ver dica

No obter, primeiro incremente a contagem no Map de estatísticas. Depois verifique se o WeakMap já tem a chave com has. Se não tiver, chame calcular(chave) e armazene com set.

Mostrar solução
function criarCache() {
  const cache = new WeakMap(); // armazena resultados
  const acessos = new Map();   // armazena contagens por chave

  return {
    obter(chave, calcular) {
      // Atualiza a contagem de acessos
      acessos.set(chave, (acessos.get(chave) || 0) + 1);

      // Se já está no cache, retorna o valor
      if (cache.has(chave)) {
        return cache.get(chave);
      }

      // Senão, calcula, armazena e retorna
      const valor = calcular(chave);
      cache.set(chave, valor);
      return valor;
    },

    estatisticas() {
      return new Map(acessos); // cópia para evitar mutações externas
    }
  };
}

// Exemplo de uso
const cache = criarCache();
const obj1 = {};
const obj2 = {};

console.log(cache.obter(obj1, () => 'valor1')); // 'valor1'
console.log(cache.obter(obj1, () => 'valor1')); // 'valor1' (do cache)
console.log(cache.obter(obj2, () => 'valor2')); // 'valor2'
console.log(cache.estatisticas()); // Map(2) { {} => 2, {} => 1 }

Passo a passo

  1. Dentro de criarCache, criamos um WeakMap chamado cache para armazenar os resultados associados a objetos chave.
  2. Criamos um Map chamado acessos para contar quantas vezes cada chave foi acessada.
  3. Retornamos um objeto com dois métodos: obter e estatisticas.
  4. No método obter, primeiro incrementamos a contagem em acessos usando set e get com fallback || 0.
  5. Verificamos se a chave já está no cache com cache.has(chave). Se sim, retornamos cache.get(chave).
  6. Se não estiver, chamamos calcular(chave), armazenamos o resultado no cache com cache.set(chave, valor) e retornamos o valor.
  7. O método estatisticas retorna uma cópia do Map de acessos para evitar que o chamador modifique o estado interno.

Por que funciona

O WeakMap é perfeito para cache porque não impede que as chaves sejam coletadas pelo garbage collector quando não forem mais referenciadas em outro lugar. Já o Map de acessos mantém uma referência forte às chaves, mas como é interno e controlado, não causa vazamento se as chaves forem removidas do uso externo? Na verdade, o Map de acessos mantém referências fortes, então as chaves não serão coletadas enquanto o cache existir. Para um cenário ideal de WeakMap, as estatísticas também deveriam usar WeakMap, mas como precisamos iterar para retornar um Map, usamos Map e assumimos que o número de chaves é gerenciado. O conceito principal é demonstrar o uso de WeakMap para cache e Map para contagem.

Erros comuns

  • Usar cache.get(chave) sem verificar has: se a chave não existir, get retorna undefined, e você pode acabar chamando calcular desnecessariamente ou retornando undefined como se fosse um valor válido.
  • Tentar iterar sobre o WeakMap: WeakMap não é iterável; se você tentar for...of ou [...cache], dará erro. Use Map para estatísticas.
  • Esquecer de incrementar a contagem antes de verificar o cache: a contagem deve incluir todos os acessos, tanto acertos quanto erros. Se incrementar só no erro, as estatísticas ficam erradas.
  • Retornar o Map interno diretamente: isso permite que o código externo modifique as contagens. Sempre retorne uma cópia (new Map(acessos)).

Outra forma de resolver

Se as chaves pudessem ser strings ou números, poderíamos usar um Map comum para o cache. Porém, para objetos, o WeakMap evita vazamentos. Uma alternativa é usar Map e limpar manualmente, mas isso é propenso a erros.

Saída esperada

valor1
valor1
valor2
Map(2) { {} => 2, {} => 1 }