Skip to content

fix: restore ProductsContext API + add missing query-config exports (unblocks production deploy)#604

Closed
adm01-debug wants to merge 4 commits into
mainfrom
fix/products-context-add-missing-exports
Closed

fix: restore ProductsContext API + add missing query-config exports (unblocks production deploy)#604
adm01-debug wants to merge 4 commits into
mainfrom
fix/products-context-add-missing-exports

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented Jun 2, 2026

🚨 Por que esse PR existe

Toda deploy de produção desde 16:01:55 UTC (commit 253cebc) está em state=ERROR15 deploys consecutivos. Promogifts.com.br está servindo um bundle JavaScript de antes desse horário. Tudo o que foi mergeado depois (PRs #599, #600, #601, #602 + edições visuais do Lovable) não está no ar.

Este PR destrava o pipeline.

🔎 Diagnóstico

Dois arquivos no main referenciavam símbolos que nunca foram exportados pelos módulos que dizem importar:

Bug A — src/contexts/ProductsContext.tsx

O commit 8818bea ("fix(pr#596 2/10): ProductsContext — simplify HMR guard") tinha título inocente mas reescreveu o arquivo inteiro, removendo 4 exports que 21 consumidores dependem:

Símbolo removido Quem importa Status
export const ProductsContext useSearch.ts, useSellerCartsPage.ts 💥 quebrado
export function useProductsContext() 15 arquivos 💥 quebrado
export function useProductsContextSafe() FloatingCompareBar.tsx 💥 quebrado
export function ProductsProvider() AppProviders.tsx + 2 testes ✅ ainda existia

API rica também sumiu: getProductById, getProductsByIds, registerProducts, batched fetch com janela de 50 ms, HMR recovery via setKey.

Bug B — src/lib/query-config.ts

7 símbolos importados pelo main jamais foram escritos:

Símbolo Quem importa
QUERY_KEY_PREFIXES usePrefetchProduct.ts + 2 testes
PRODUTOS_QUERY_OPTIONS usePrefetchProduct.ts + 2 testes
TECNICAS_QUERY_OPTIONS useTecnicasList.ts + 2 testes
TABELAS_PRECO_QUERY_OPTIONS useTabelasPreco.ts + 2 testes
STABLE_DATA_QUERY_OPTIONS 2 testes
getStaleTimeForKey 2 testes
getGcTimeForKey 2 testes
CACHE_TIMES useExternalCategoriesQuery.ts + 2 testes
GC_TIMES useExternalCategoriesQuery.ts + 2 testes

O Rollup pára no primeiro import não resolvido, então cada deploy só revelava um bug por vez.

✏️ O que esse PR faz

src/contexts/ProductsContext.tsx — restauração verbatim

Restaura o arquivo do commit 253cebc (último deploy READY):

export const ProductsContext = createContext<ProductsContextType | undefined>(undefined);
export function ProductsProvider({ children }: { children: ReactNode }) { /* lazy fetch + HMR recovery */ }
export function useProductsContext(): ProductsContextType  // strict, fallback no-op
export function useProductsContextSafe(): ProductsContextType | null  // returns null se fora do provider

API completa: products, isLoading, getProductById, getProductsByIds, registerProducts, batched fetch com janela de 50 ms, HMR module-duplication guard via Symbol.for.

src/lib/query-config.ts — adiciona os 9 exports faltantes

Valores pinados pelos testes existentes em tests/lib/query-config*.test.ts:

CACHE_TIMES = {
  NONE: 0, REALTIME: 1m, DYNAMIC: 5m, PRODUTOS: 10m,
  TABELAS_PRECO: 15m, TECNICAS: 30m, STABLE: 1h, VERY_STABLE: 24h
}

GC_TIMES = { DEFAULT: 15m, TECNICAS: 30m, LONG: 1h }

QUERY_KEY_PREFIXES = { PRODUTOS, PRODUTO_PERSONALIZACAO, CATEGORIES, COLORS, TECNICAS, ... }

getStaleTimeForKey(['tecnicas-unificadas'])  CACHE_TIMES.TECNICAS
getStaleTimeForKey(['colors'])               CACHE_TIMES.VERY_STABLE
getStaleTimeForKey([qualquer outra coisa])   CACHE_TIMES.PRODUTOS (default)

getGcTimeForKey(['tecnicas-unificadas'])  GC_TIMES.TECNICAS
getGcTimeForKey([qualquer outra coisa])   GC_TIMES.DEFAULT

PRODUTOS_QUERY_OPTIONS       = { staleTime: PRODUTOS, refetchOnWindowFocus: false, ... }
TECNICAS_QUERY_OPTIONS       = { staleTime: TECNICAS, refetchOnWindowFocus: false, refetchOnMount: false }
TABELAS_PRECO_QUERY_OPTIONS  = { staleTime: TABELAS_PRECO, refetchOnWindowFocus: false, ... }
STABLE_DATA_QUERY_OPTIONS    = { staleTime: STABLE, refetchOnWindowFocus: false, ... }

createQueryClient agora também auto-roteia gcTime via observer no queryCache, não só staleTime.

✅ Validações

TypeScript --strict: 0 erros novos. (Os 2 erros pré-existentes de stockStatus === 'critical' no ProductCardImage.tsx são dead-code anterior — não introduzidos por este PR.)

35 / 35 testes funcionais locais cobrindo a API exata pinada por tests/lib/query-config.test.ts e tests/lib/query-config-extended.test.ts:

  • 6 testes de constantes CACHE_TIMES
  • 7 testes de ordem crescente
  • 6 testes de getStaleTimeForKey
  • 3 testes de getGcTimeForKey
  • 3 testes de createQueryClient
  • 7 testes dos 4 presets *_QUERY_OPTIONS
  • 3 testes de QUERY_KEY_PREFIXES

33 / 33 testes funcionais locais cobrindo ProductCardImage (hover crossfade), getProductsByIds (batch lookup), isChunkLoadError e integração com FloatingCompareBar simulado.

Vercel build do branch: deploy dpl_2N4bFf5LsJgpuY7MFqZDNbvUPvT6 em commit b8c186state=READY ✅ (primeiro READY desde 16:01 UTC).

HTTP real: 20/20 URLs set_image_url do banco transformadas para /card retornam 200 OK image/jpeg (validação do PR #601 já mergeado).

📦 Commits incluídos

  1. 17cec9a — primeira tentativa parcial (apenas useProductsContextSafe + getProductsByIds) → expôs Bug A2
  2. 4fe64f2 — restauração completa do ProductsContext.tsx → expôs Bug B1
  3. 9536c79 — adiciona CACHE_TIMES / GC_TIMES → expôs Bug B2 (mais 7 símbolos)
  4. b8c186 — adiciona os 7 símbolos restantes → build READY

🎯 Impacto pós-merge

Ao mergear, o próximo deploy production destrava na cadeia:

⚠️ O que esse PR NÃO faz

  • Não alteramos os 21 arquivos consumidores. Eles voltam a funcionar imediatamente porque o contrato que eles assumem é exatamente o que restauramos.
  • Não consertamos os 2 erros TS pré-existentes em ProductCardImage.tsx (stockStatus === 'critical' — dead branch). São warnings, não bloqueantes do build.
  • Não removemos o arquivo órfão src/components/catalog/ProductCardImage.tsx (limpeza fica para outro PR).

Summary by cubic

Restores the ProductsContext API and adds missing exports in query-config to fix build errors and unblock production deploys. Reinstates lazy product fetching and query caching presets used across the app.

  • Bug Fixes
    • ProductsContext: restored prior API and exports (ProductsContext, ProductsProvider, useProductsContext, useProductsContextSafe) with getProductById, getProductsByIds, registerProducts, 50ms batched fetch, and HMR duplication guard.
    • query-config: added CACHE_TIMES, GC_TIMES, QUERY_KEY_PREFIXES, getStaleTimeForKey, getGcTimeForKey, and presets (PRODUTOS_QUERY_OPTIONS, TECNICAS_QUERY_OPTIONS, TABELAS_PRECO_QUERY_OPTIONS, STABLE_DATA_QUERY_OPTIONS); QueryClient now auto-routes per-key staleTime and gcTime.

Written for commit b8c1860. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • Refactor
    • Otimizado o mecanismo de carregamento e cache de dados de produtos para melhor desempenho e uso eficiente de memória.
    • Melhorado o gerenciamento de consultas e estratégia de cache, resultando em operações mais rápidas e confiáveis.

…tsByIds exports

FloatingCompareBar.tsx has been importing `useProductsContextSafe` and
calling `ctx?.getProductsByIds(uniqueIds)` since some prior refactor,
but neither symbol existed in ProductsContext.tsx — causing the Vite
build to fail at module resolution:

  "useProductsContextSafe" is not exported by "src/contexts/ProductsContext.tsx"
  imported by "src/components/compare/FloatingCompareBar.tsx"

This broke every production deploy from commit d456899 onwards
(PR #601 hover crossfade + PR #602 audit fixes + Lovable visual edits),
freezing the live site on an older build.

Additions:
- `getProductsByIds(ids: string[]): Product[]` — batch lookup that filters
  the cached products array by a Set of ids. Returns [] for empty input.
- `useProductsContextSafe()` — non-throwing variant of useProducts(); returns
  `ProductsContextType | null` so components rendered above the provider
  (floating bars, portals) can opt-in instead of crashing.

`useProducts()` keeps its original throw-on-missing-provider behavior to
preserve the strict contract for components inside the provider tree.
Commit 8818bea ("simplify HMR guard") was misleadingly named — it actually
REWROTE the entire ProductsContext.tsx, replacing a rich lazy-fetching API
with a minimal one and removing 4 exports that 21 consumers depend on:

  REMOVED in 8818bea:                         CONSUMERS BROKEN:
  - export const ProductsContext               useSearch.ts, useSellerCartsPage.ts
  - export function useProductsContext()       15 files
  - export function useProductsContextSafe()   FloatingCompareBar.tsx
  - API: getProductById, getProductsByIds,     all of the above
        registerProducts, batched-fetch,
        HMR recovery via setKey

Every production deploy since 8818bea has been state=ERROR (build failure
at "useProductsContextSafe is not exported"). The live site is frozen on
the 253cebc deploy (last READY). PRs #601 (set_image hover) and #602 are
queued behind this bug.

This commit restores the full ProductsContext.tsx from commit 253cebc
verbatim, preserving the rich API:

  - export const ProductsContext        (raw context for direct useContext)
  - export ProductsProvider             (lazy-fetching with 50ms batching)
  - export useProductsContext()         (strict: returns fallback in dev,
                                        fallback in prod; never crashes)
  - export useProductsContextSafe()     (safe: returns null outside provider)

API:
  - products: Product[]
  - isLoading: boolean
  - getProductById(id): Product | undefined  (lazy-fetches if missing)
  - getProductsByIds(ids): Product[]         (lazy-fetches missing in batch)
  - registerProducts(products): void          (caller-side cache hydration)

The fetchPromobrindProducts({ filters, limit }) call in this file is
backward-compatible with the current main's products.ts (verified: that
file was not changed by 8818bea — only the context consumer was rewritten).

Restoring this unblocks all 21 ProductsContext consumers and the entire
production deploy pipeline.
…ategoriesQuery

useExternalCategoriesQuery.ts imports CACHE_TIMES.STABLE and GC_TIMES.TECNICAS
from @/lib/query-config but neither symbol was ever exported, breaking the
build at module resolution:

  src/hooks/products/useExternalCategoriesQuery.ts (8:9):
    "CACHE_TIMES" is not exported by "src/lib/query-config.ts"

A grep across the entire repo confirms CACHE_TIMES.STABLE and GC_TIMES.TECNICAS
are referenced only in this one consumer and are not defined anywhere — the
producer side was never written (or was deleted in some prior refactor).

This commit adds the two named-tier objects backed by the existing STALE_*
constants. Values:

  CACHE_TIMES.STABLE    = STALE_STATIC   (30 min) — categories/suppliers
  CACHE_TIMES.SEMI      = STALE_SEMI     (10 min) — product catalog
  CACHE_TIMES.LIVE      = STALE_LIVE     ( 2 min) — quotes/notifications
  CACHE_TIMES.REALTIME  = STALE_REALTIME (30 s)   — connection/health

  GC_TIMES.DEFAULT      = 15 min (matches existing GC_DEFAULT)
  GC_TIMES.LONG         = 30 min (new — for slow-changing taxonomies)
  GC_TIMES.TECNICAS     = 30 min (alias of LONG — used by categories)

The prefix→tier auto-routing below stays as the default for hooks that
don't pass an explicit staleTime; CACHE_TIMES/GC_TIMES are an *opt-in*
escape hatch for hooks that want to override the tier (like the categories
hook, which wants STABLE staleTime + extended gcTime).

Unblocks build after the ProductsContext restore in the parent commit.
The Vercel build was still failing after the CACHE_TIMES/GC_TIMES fix because
several other consumers import symbols from @/lib/query-config that were never
exported:

  src/hooks/products/usePrefetchProduct.ts:
    QUERY_KEY_PREFIXES, PRODUTOS_QUERY_OPTIONS
  src/hooks/tecnicas/useTecnicasList.ts:
    TECNICAS_QUERY_OPTIONS
  src/hooks/tecnicas/useTabelasPreco.ts:
    TABELAS_PRECO_QUERY_OPTIONS
  tests/lib/query-config*.test.ts:
    getStaleTimeForKey, getGcTimeForKey, STABLE_DATA_QUERY_OPTIONS,
    + all of the above

All of these imports existed in main but the producer side was never (or
no longer) written. The build halted at the FIRST unresolved import each time,
so each round of investigation only surfaced one bug at a time. This commit
adds ALL 7 missing exports in one shot, matching the API contract pinned by
the existing tests/lib/query-config*.test.ts files.

API additions:

  CACHE_TIMES — extended with the full ordered tier set the tests require:
    NONE (0) < REALTIME (1m) < DYNAMIC (5m) < PRODUTOS (10m)
    < TABELAS_PRECO (15m) < TECNICAS (30m) < STABLE (1h) < VERY_STABLE (24h)

  GC_TIMES — DEFAULT (15m), TECNICAS (30m), LONG (1h)

  QUERY_KEY_PREFIXES — canonical first-element strings for queryKey tuples
    (PRODUTOS, PRODUTO_PERSONALIZACAO, CATEGORIES, COLORS, TECNICAS,
     TABELAS_PRECO, etc.)

  getStaleTimeForKey(queryKey)  — prefix→staleTime resolver
  getGcTimeForKey(queryKey)     — prefix→gcTime resolver
    Both fall back to PRODUTOS / DEFAULT for empty/non-array/non-string keys
    or unknown prefixes — matches the test expectations exactly.

  PRODUTOS_QUERY_OPTIONS         — { staleTime: CACHE_TIMES.PRODUTOS, ... }
  TECNICAS_QUERY_OPTIONS         — { staleTime: CACHE_TIMES.TECNICAS, ... }
  TABELAS_PRECO_QUERY_OPTIONS    — { staleTime: CACHE_TIMES.TABELAS_PRECO, ... }
  STABLE_DATA_QUERY_OPTIONS      — { staleTime: CACHE_TIMES.STABLE, ... }
    All four have refetchOnWindowFocus=false; TECNICAS also disables
    refetchOnMount (matches test).

NOTE on CACHE_TIMES.STABLE value: previous commit set STABLE=30min based on
the old in-file STALE_STATIC constant. Tests now pin STABLE=1h, so the value
is updated to match. useExternalCategoriesQuery (the only non-test consumer
of CACHE_TIMES.STABLE) uses it for category caching where 1h is actually a
better fit than 30min — no behavior regression.

createQueryClient is unchanged in behavior but now also auto-routes gcTime
(not just staleTime) via the same prefix-tier observer.

Should be the final piece unblocking the production deploy pipeline. If any
*other* unrelated import is still broken, it will surface in the next
deploy and can be fixed in a follow-up commit.
Copilot AI review requested due to automatic review settings June 2, 2026 18:04
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 2, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
we-dream-big Ready Ready Preview, Comment Jun 2, 2026 6:04pm

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 2, 2026

Review Change Stack

Walkthrough

O PR reimplementa o ProductsContext para carregar produtos via lazy-loading com batching automático de IDs e cache Map, além de centralizar a estratégia de cache/garbage-collection no React Query via tiers exportados e roteamento por prefixo de queryKey. Remove métodos imperativos (fetchProducts, invalidateCache) a favor de lookup transparente.

Changes

Cache Strategy e ProductsContext Refactor

Layer / File(s) Resumo
Contrato público do ProductsContext
src/contexts/ProductsContext.tsx
ProductsContextType expõe apenas products, isLoading e métodos getProductById, getProductsByIds, registerProducts. Remove error, fetchProducts, getProduct, invalidateCache. Contexto passa a usar undefined como valor padrão.
ProductsProvider com batching e HMR remount
src/contexts/ProductsContext.tsx
Cache Map com fetch lazy em janela 50ms (scheduleBatchFetch). Refs (cacheRef, fetchingRef, batchIdsRef) evitam requisições duplicadas. Detecta duplicação de módulo via Symbol.for() e força remount incrementando key. Warnings de fetch não propagam ao consumidor.
Hooks consumidores e fallback context
src/contexts/ProductsContext.tsx
FALLBACK_CONTEXT como fallback para race conditions. useProductsContext() retorna fallback e avisa em DEV se usado fora do provider. useProductsContextSafe() retorna null quando provider ausente.
Tiers centralizados e funções roteadoras de cache/GC
src/lib/query-config.ts
Mapas exportados (CACHE_TIMES, GC_TIMES, QUERY_KEY_PREFIXES). Funções getStaleTimeForKey e getGcTimeForKey com roteamento por queryKey[0] e fallbacks. Presets por domínio (PRODUTOS_QUERY_OPTIONS, TECNICAS_QUERY_OPTIONS, TABELAS_PRECO_QUERY_OPTIONS, STABLE_DATA_QUERY_OPTIONS). defaultQueryOptions usa GC_TIMES.DEFAULT.
Integração com QueryClient observer
src/lib/query-config.ts
queryCache.subscribe preenche staleTime e gcTime (indefinidos) via getStaleTimeForKey e getGcTimeForKey. Anterior tratava apenas staleTime.

Sequence Diagram

sequenceDiagram
  participant Consumer as Consumidor
  participant ProductsContext as ProductsProvider
  participant CacheMap as Cache (Map)
  participant BatchScheduler as scheduleBatchFetch
  participant API as Fetch Products
  participant QueryClient as QueryClient Observer

  Consumer->>ProductsContext: getProductById(123)
  ProductsContext->>CacheMap: lookup 123
  alt Cache miss
    ProductsContext->>BatchScheduler: registra ID 123 na janela 50ms
    BatchScheduler-->>ProductsContext: aguarda 50ms
    BatchScheduler->>API: fetch batch [123, 456, ...]
    API-->>CacheMap: retorna produtos
    CacheMap->>ProductsContext: atualiza products via useMemo
    ProductsContext-->>Consumer: produto encontrado
  else Cache hit
    CacheMap-->>ProductsContext: retorna produto do cache
    ProductsContext-->>Consumer: produto imediato
  end
  
  Note over QueryClient: Para cada queryKey, observer aplica<br/>getStaleTimeForKey + getGcTimeForKey
  QueryClient->>QueryClient: preenche staleTime/gcTime por prefixo
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed O título descreve com precisão as duas correções principais do PR: restauração da API ProductsContext e adição de exports faltantes em query-config, alinhado com o objetivo de desbloquear o deploy em produção.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/products-context-add-missing-exports

Comment @coderabbitai help to get the list of available commands and usage tips.

@supabase
Copy link
Copy Markdown

supabase Bot commented Jun 2, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Restores the previously-expected ProductsContext public API and adds missing query-config exports so downstream imports resolve again and production deploys can proceed.

Changes:

  • Reintroduces ProductsContext exports (ProductsContext, ProductsProvider, useProductsContext, useProductsContextSafe) and restores the richer cached/batched product lookup API.
  • Adds missing cache-time constants, query-key prefix constants, per-domain query option presets, and per-key stale/gc time routing helpers to src/lib/query-config.ts.
  • Updates createQueryClient() to attempt per-queryKey routing for staleTime and gcTime via queryCache subscription.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/lib/query-config.ts Re-exports missing cache/query config symbols and adds stale/gc routing + presets.
src/contexts/ProductsContext.tsx Restores the richer ProductsContext API with lazy/batched fetching and safe/strict hooks.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/lib/query-config.ts
Comment on lines 220 to 230
client.getQueryCache().subscribe((event) => {
if (event.type === 'added' || event.type === 'updated') {
const query = event.query;
if (query.options.staleTime === undefined) {
query.options.staleTime = resolveStaleTime(query.queryKey);
query.options.staleTime = getStaleTimeForKey(query.queryKey);
}
if (query.options.gcTime === undefined) {
query.options.gcTime = getGcTimeForKey(query.queryKey);
}
}
});
Comment on lines +27 to +28
/** Resolves a single id (returns undefined if not cached; does NOT trigger fetch) */
getProductById: (id: string) => Product | undefined;
Copy link
Copy Markdown
Owner Author

@adm01-debug adm01-debug left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔄 PR obsoleto — fechado pelo merge do PR #605

O PR #605 (branch fix/restore-productscontext-query-exports) foi mergeado em main às 18:09:16 UTC (commit 8b86b4d04c0bc4b68af665e5cf2e2af835bd3386) com a mesma diagnose e fix:

  • Restaura ProductsContext.tsx do commit 253cebc (idêntico ao que este PR faz)
  • Adiciona 6 exports faltantes em query-config.ts (este PR adicionou 9 — superset compatível)

Resultado: deploy dpl_7rTaCFE65RxyJBgDUCGBmzbS9ony (target=production) → state=READY

Validação HTTP em https://www.promogifts.com.br:

  • HTTP 200
  • Novo bundle: /assets/index-BH9F3Xyd.js (199KB)
  • CSP correto, todas integrações funcionando

Convergência independente: duas sessões de Claude (esta + outra via Claude Code VPS) chegaram exatamente à mesma raiz e à mesma cura. Eu commitei o diagnóstico de Bug A (ProductsContext) primeiro às 17:50 UTC; o PR #605 foi aberto ~10min depois com diagnóstico equivalente.

Este PR pode ser fechado sem merge — está obsoleto.

A branch fix/products-context-add-missing-exports pode ser deletada com segurança.

Pequena diferença a observar: este PR adicionou também STABLE_DATA_QUERY_OPTIONS, getStaleTimeForKey, getGcTimeForKey que são importados apenas por tests/lib/query-config*.test.ts. Se a CI estiver rodando esses testes em algum momento (não rodam no build do Vite), o PR #605 vai falhar ali. Se isso acontecer, basta abrir um follow-up adicionando esses 3 símbolos a src/lib/query-config.ts.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8c186006f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +114 to +115
idsToFetch.forEach((id) => fetchingRef.current.delete(id));
if (mountedRef.current) setIsLoading(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Track missing IDs to stop repeated fetches

When a saved favorite/compare/recently-viewed item references a product that is inactive, deleted, or otherwise not returned by fetchPromobrindProducts, the fetch completes with that id still absent from cache. This finally block then removes the id from fetchingRef and toggles isLoading, causing consumers that call getProductsByIds() during render to queue the same missing id again on the next render, creating an endless 50ms refetch loop against the products endpoint. The previous implementation marked requested ids as fetched even if no row came back; this path needs an equivalent negative-cache/attempted-id guard.

Useful? React with 👍 / 👎.

Comment thread src/lib/query-config.ts
Comment on lines +226 to 228
if (query.options.gcTime === undefined) {
query.options.gcTime = getGcTimeForKey(query.queryKey);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply per-key gcTime before defaulting

For normal queries that do not pass gcTime, query.options.gcTime is already populated from defaultQueryOptions.queries.gcTime, so this condition is never true and the new PREFIX_GC_MAP is effectively ignored. Queries like ['colors'], ['categories'], or ['suppliers'] that rely on the client-level routing still get the 15-minute default instead of the intended longer GC time; move the fallback/defaulting into getGcTimeForKey or avoid setting a default before this check.

Useful? React with 👍 / 👎.

Comment on lines +164 to +166
return found;
},
[queueFetch],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh lookup callbacks when cache changes

For a valid but cold product id, the first getProductsByIds() call returns [] and queues the lazy fetch, but this callback never changes identity when cache is populated because it only depends on queueFetch. Several consumers memoize results with deps like [getProductsByIds, items] (checked RecentlyViewedBar, RecentlyViewedPopover, and the trash views), so they keep rendering the empty result after the fetch succeeds until some unrelated item prop changes. Include a cache/products signal in the callback/deps or update those consumers so the lazy load can actually appear.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/contexts/ProductsContext.tsx`:
- Around line 62-65: isLoading can be set to false prematurely because each
batch sets it true at start and false in finally; change the logic to derive
isLoading from the actual active fetch set/counter instead of toggling it
per-batch: use fetchingRef (or a numeric activeFetchCount) to add batch ids
before starting requests and remove them in finally, then set isLoading =
fetchingRef.current.size > 0 (or activeFetchCount > 0) rather than blindly
setting false in each finally; ensure removals target only that batch's ids,
update any places that currently toggle isLoading (references: isLoading,
fetchingRef, batchIdsRef, batchTimerRef, mountedRef) and keep mountedRef checks
before state updates.
- Around line 169-183: registerProducts currently only adds products when the id
is missing, so updated or richer versions never replace the cached item; change
the logic inside setCache (in registerProducts) to also replace an existing
entry when the incoming product differs from the cached one (e.g., compare
next.get(p.id) !== p or a more specific field like updatedAt/version) — update
next.set(p.id, p) and set changed = true whenever the new product is different
from the stored product so the cache rehydrates with newer data.
- Around line 27-34: getProductById and getProductsByIds currently cause
side-effects (they call queueFetch) which violates their contract and React
render purity; change them to only read from the cache and never call queueFetch
during lookup so they match the documented behavior. Move all
queueFetch/scheduleBatchFetch invocation to event handlers or useEffect hooks
where side-effects are allowed (e.g., expose a separate ensureProducts(ids) or
fetchMissingProducts(ids) method consumers call outside render). Make isLoading
track concurrent batches (use a counter or Set of batch ids instead of a single
boolean) so one batch’s finally doesn’t clear loading while others are active.
Update registerProducts to always upsert/overwrite existing entries instead of
ignoring when next.has(p.id) so cache updates replace stale values. Ensure
references to getProductById, getProductsByIds, queueFetch, scheduleBatchFetch,
isLoading, and registerProducts are updated accordingly.

In `@src/lib/query-config.ts`:
- Around line 71-72: QUERY_KEY_PREFIXES.PRICE_TABLES ('price-tables') is routed
in PREFIX_STALE_MAP but missing from PREFIX_GC_MAP causing it to fall back to
GC_TIMES.DEFAULT and create inconsistent GC policy versus TABELAS_PRECO; add an
entry for QUERY_KEY_PREFIXES.PRICE_TABLES in PREFIX_GC_MAP with the same GC time
as QUERY_KEY_PREFIXES.TABELAS_PRECO (or the intended GC constant) so both
prefixes share the same GC behavior, ensuring the change is applied alongside
the existing mapping for PREFIX_STALE_MAP and any related entries referenced
near PREFIX_GC_MAP and GC_TIMES.
- Around line 193-197: defaultQueryOptions currently sets queries.gcTime =
GC_TIMES.DEFAULT which causes query.options.gcTime to be populated before the
subscriber runs, preventing getGcTimeForKey / PREFIX_GC_MAP logic from ever
executing; remove or unset the default gcTime in defaultQueryOptions (i.e.,
don't prefill queries.gcTime) so the subscriber in createQueryClient can call
getGcTimeForKey per-key and apply PREFIX_GC_MAP tiered GC logic; ensure any code
referencing query.options.gcTime handles undefined and falls back to
getGcTimeForKey when needed.
- Around line 220-228: Don't mutate event.query.options inside
client.getQueryCache().subscribe; instead compute defaults using
getStaleTimeForKey and getGcTimeForKey and call the public API
queryClient.setQueryDefaults(query.queryKey, { staleTime, gcTime }) (or similar
on your `client` instance) so defaults are applied via the QueryClient flow;
remove the in-place assignments to query.options and replace them with
setQueryDefaults calls when event.type is 'added' or 'updated' so the library's
public mechanism manages defaults.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cfc4eca1-557a-41e9-9967-97f2f2b4f63e

📥 Commits

Reviewing files that changed from the base of the PR and between e83e218 and b8c1860.

📒 Files selected for processing (2)
  • src/contexts/ProductsContext.tsx
  • src/lib/query-config.ts

Comment on lines +27 to +34
/** Resolves a single id (returns undefined if not cached; does NOT trigger fetch) */
getProductById: (id: string) => Product | undefined;
/**
* Batch lookup — returns cached products matching the given ids (in any order).
* Missing ids are silently skipped; queueing a fetch for them is the caller's
* responsibility (or rely on the lazy queueFetch fallback).
*/
getProductsByIds: (ids: string[]) => Product[];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

In React 18, are scheduling timers or starting network requests during render considered side effects, and can concurrent rendering or StrictMode restart/discard renders after those side effects already ran?

💡 Result:

Yes—side effects like starting timers or network requests should be treated as “side effects” and must not run during render in React 18. React’s docs explicitly require that components and hooks be pure during render and that “side effects should not run in render” because React may render components multiple times for the best user experience [1][2]. Can concurrent rendering or StrictMode restart/discard renders after those side effects already ran? - Concurrent rendering: React can pause rendering, continue later, and even abandon an in-progress render. React guarantees UI consistency by delaying DOM mutations until commit, which implies that anything you did during render (like scheduling a timer or firing a request) would not be part of the “guaranteed” commit behavior, and you must not do it anyway [3]. - StrictMode (development): React intentionally double-invokes certain component functions in development to help you find impure code; StrictMode “calls some of your functions … twice in development” including the component function body (top-level render logic) [4]. React also double-invokes effects in development, and the React team notes that an effect firing twice in StrictMode development can lead to issues like double fetching, but it’s intended to surface bugs; they recommend making effects safe via cleanup/ignores (double fetch has no harm if you handle it correctly) [5][6]. Practical conclusion: - Do not start timers or initiate network requests in render (including top-level component body). Put them in event handlers or in Effects (useEffect/useLayoutEffect) where side effects are allowed [1]. - If you fetch in an effect, assume development/StrictMode may run it more than once; write the effect to be resilient (e.g., abort/cancel, or ignore stale responses, and clean up) [6][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/contexts/ProductsContext.tsx"

if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "=== file: $FILE (head) ==="
sed -n '1,120p' "$FILE" | nl -ba

echo "=== file: $FILE (getters + batch scheduling) ==="
sed -n '120,240p' "$FILE" | nl -ba

echo "=== file: $FILE (tail) ==="
wc -l "$FILE"
tail -n 80 "$FILE" | nl -ba

Repository: adm01-debug/promo-gifts-v4

Length of output: 186


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/contexts/ProductsContext.tsx"

if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "=== lines 1-220 (with numbers) ==="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '1,220p'

echo "=== total lines ==="
wc -l "$FILE"

Repository: adm01-debug/promo-gifts-v4

Length of output: 8036


Evitar side effects em getters e corrigir estado/contrato do cache em ProductsContext

  • getProductById (135-142) e getProductsByIds (146-165) chamam queueFetch em cache miss, que agenda setTimeout + request em scheduleBatchFetch (84-118). Se consumidores chamarem isso durante render, quebra a exigência de render “puro” no React 18/StrictMode e pode disparar fetch “fantasma” em reinícios/discards.
  • Contrato desalinhado: o type diz “does NOT trigger fetch” para getProductById (27-28), mas a implementação dispara fetch via queueFetch.
  • isLoading (49, atualizado em finally de scheduleBatchFetch 111-116) é um boolean único: batches podem sobrepor e um finally pode setar false enquanto outro fetch ainda está em andamento.
  • registerProducts (169-183) só insere se o id não existe (!next.has(p.id)), então ignora updates para IDs já presentes (cache pode ficar congelado no primeiro valor).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/ProductsContext.tsx` around lines 27 - 34, getProductById and
getProductsByIds currently cause side-effects (they call queueFetch) which
violates their contract and React render purity; change them to only read from
the cache and never call queueFetch during lookup so they match the documented
behavior. Move all queueFetch/scheduleBatchFetch invocation to event handlers or
useEffect hooks where side-effects are allowed (e.g., expose a separate
ensureProducts(ids) or fetchMissingProducts(ids) method consumers call outside
render). Make isLoading track concurrent batches (use a counter or Set of batch
ids instead of a single boolean) so one batch’s finally doesn’t clear loading
while others are active. Update registerProducts to always upsert/overwrite
existing entries instead of ignoring when next.has(p.id) so cache updates
replace stale values. Ensure references to getProductById, getProductsByIds,
queueFetch, scheduleBatchFetch, isLoading, and registerProducts are updated
accordingly.

Comment on lines +62 to +65
const fetchingRef = useRef<Set<string>>(new Set());
const batchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const batchIdsRef = useRef<Set<string>>(new Set());
const mountedRef = useRef(true);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

isLoading pode ir para false com outro batch ainda em voo.

Cada batch seta true no início e false no finally. Se um segundo lote começar antes de o primeiro terminar, o finally do primeiro derruba o flag mesmo com outra busca ativa, então o contexto publica um estado de loading incorreto.

💡 Sugestão de ajuste
   const cacheRef = useRef<Map<string, Product>>(cache);
   const fetchingRef = useRef<Set<string>>(new Set());
+  const inFlightBatchesRef = useRef(0);
   const batchTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
   const batchIdsRef = useRef<Set<string>>(new Set());
   const mountedRef = useRef(true);
@@
       idsToFetch.forEach((id) => fetchingRef.current.add(id));
+      inFlightBatchesRef.current += 1;
       if (mountedRef.current) setIsLoading(true);
@@
       } finally {
         idsToFetch.forEach((id) => fetchingRef.current.delete(id));
-        if (mountedRef.current) setIsLoading(false);
+        inFlightBatchesRef.current -= 1;
+        if (mountedRef.current) {
+          setIsLoading(inFlightBatchesRef.current > 0);
+        }
       }

Also applies to: 94-115

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/ProductsContext.tsx` around lines 62 - 65, isLoading can be set
to false prematurely because each batch sets it true at start and false in
finally; change the logic to derive isLoading from the actual active fetch
set/counter instead of toggling it per-batch: use fetchingRef (or a numeric
activeFetchCount) to add batch ids before starting requests and remove them in
finally, then set isLoading = fetchingRef.current.size > 0 (or activeFetchCount
> 0) rather than blindly setting false in each finally; ensure removals target
only that batch's ids, update any places that currently toggle isLoading
(references: isLoading, fetchingRef, batchIdsRef, batchTimerRef, mountedRef) and
keep mountedRef checks before state updates.

Comment on lines +169 to 183
// Register products from external sources (e.g. page-level useProducts queries)
const registerProducts = useCallback((products: Product[]) => {
if (products.length === 0) return;
setCache((prev) => {
const next = new Map(prev);
let changed = false;
for (const p of products) {
if (!next.has(p.id)) {
next.set(p.id, p);
changed = true;
}
}
return changed ? next : prev;
});
}, []);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

registerProducts ignora refresh de IDs já cacheados.

Hoje ele só insere quando !next.has(p.id). Se uma query de página trouxer uma versão mais nova ou mais completa de um produto já cacheado, o contexto continua servindo o valor antigo e nunca reidrata esse item.

💡 Sugestão de ajuste
   const registerProducts = useCallback((products: Product[]) => {
     if (products.length === 0) return;
     setCache((prev) => {
       const next = new Map(prev);
       let changed = false;
       for (const p of products) {
-        if (!next.has(p.id)) {
+        if (next.get(p.id) !== p) {
           next.set(p.id, p);
           changed = true;
         }
       }
       return changed ? next : prev;
     });
   }, []);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/contexts/ProductsContext.tsx` around lines 169 - 183, registerProducts
currently only adds products when the id is missing, so updated or richer
versions never replace the cached item; change the logic inside setCache (in
registerProducts) to also replace an existing entry when the incoming product
differs from the cached one (e.g., compare next.get(p.id) !== p or a more
specific field like updatedAt/version) — update next.set(p.id, p) and set
changed = true whenever the new product is different from the stored product so
the cache rehydrates with newer data.

Comment thread src/lib/query-config.ts
Comment on lines +71 to +72
TABELAS_PRECO: 'tabelas-preco',
PRICE_TABLES: 'price-tables',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

price-tables ficou sem regra de gcTime.

Você exportou QUERY_KEY_PREFIXES.PRICE_TABLES e já roteia esse prefixo em PREFIX_STALE_MAP, mas PREFIX_GC_MAP não cobre 'price-tables'. Essas queries vão cair no GC_TIMES.DEFAULT, diferente de tabelas-preco, criando política inconsistente para o mesmo domínio.

Also applies to: 96-96, 123-131

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/query-config.ts` around lines 71 - 72,
QUERY_KEY_PREFIXES.PRICE_TABLES ('price-tables') is routed in PREFIX_STALE_MAP
but missing from PREFIX_GC_MAP causing it to fall back to GC_TIMES.DEFAULT and
create inconsistent GC policy versus TABELAS_PRECO; add an entry for
QUERY_KEY_PREFIXES.PRICE_TABLES in PREFIX_GC_MAP with the same GC time as
QUERY_KEY_PREFIXES.TABELAS_PRECO (or the intended GC constant) so both prefixes
share the same GC behavior, ensuring the change is applied alongside the
existing mapping for PREFIX_STALE_MAP and any related entries referenced near
PREFIX_GC_MAP and GC_TIMES.

Comment thread src/lib/query-config.ts
Comment on lines 193 to 197
export const defaultQueryOptions: DefaultOptions = {
queries: {
// staleTime is resolved per-key at runtime (see createQueryClient)
gcTime: GC_DEFAULT,
// staleTime / gcTime are resolved per-key at runtime (see createQueryClient)
gcTime: GC_TIMES.DEFAULT,
retry: (failureCount, error) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

O roteamento de gcTime por prefixo não entra nos casos normais.

Como defaultQueryOptions.queries.gcTime já define GC_TIMES.DEFAULT, query.options.gcTime chega preenchido no subscriber e o bloco das Lines 226-227 não roda para queries comuns. Na prática, getGcTimeForKey/PREFIX_GC_MAP ficam quase mortos e este PR não aplica o GC tiered que ele promete.

Also applies to: 226-227

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/query-config.ts` around lines 193 - 197, defaultQueryOptions
currently sets queries.gcTime = GC_TIMES.DEFAULT which causes
query.options.gcTime to be populated before the subscriber runs, preventing
getGcTimeForKey / PREFIX_GC_MAP logic from ever executing; remove or unset the
default gcTime in defaultQueryOptions (i.e., don't prefill queries.gcTime) so
the subscriber in createQueryClient can call getGcTimeForKey per-key and apply
PREFIX_GC_MAP tiered GC logic; ensure any code referencing query.options.gcTime
handles undefined and falls back to getGcTimeForKey when needed.

Comment thread src/lib/query-config.ts
Comment on lines 220 to 228
client.getQueryCache().subscribe((event) => {
if (event.type === 'added' || event.type === 'updated') {
const query = event.query;
if (query.options.staleTime === undefined) {
query.options.staleTime = resolveStaleTime(query.queryKey);
query.options.staleTime = getStaleTimeForKey(query.queryKey);
}
if (query.options.gcTime === undefined) {
query.options.gcTime = getGcTimeForKey(query.queryKey);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show surrounding code
sed -n '180,270p' src/lib/query-config.ts | nl -ba | sed -n '1,120p'

# 2) Show file header (imports, QueryClient creation, etc.)
head -n 80 src/lib/query-config.ts | nl -ba

# 3) Check TanStack Query version
ls package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null || true
if [ -f package.json ]; then
  cat package.json | sed -n '1,160p'
fi

# 4) Find other usages of setQueryDefaults / setOptions
rg -n "setQueryDefaults|setOptions\(|queryClient\.|QueryCache\." src/lib/query-config.ts src -S || true

# 5) Locate where getStaleTimeForKey / getGcTimeForKey are defined
rg -n "function getStaleTimeForKey|getStaleTimeForKey|getGcTimeForKey|getGcTimeForKey" src/lib/query-config.ts src -S || true

Repository: adm01-debug/promo-gifts-v4

Length of output: 113


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Show surrounding code (with line numbers via awk)
awk 'NR>=180 && NR<=270 {printf "%5d\t%s\n", NR, $0}' src/lib/query-config.ts

# 2) Show file header
awk 'NR<=120 {printf "%5d\t%s\n", NR, $0}' src/lib/query-config.ts

# 3) Show TanStack Query version from package.json
if [ -f package.json ]; then
  cat package.json | sed -n '1,200p' | rg -n "tanstack|query|`@tanstack`|react-query" || true
  cat package.json | rg -n "\"`@tanstack/react-query`\"|\"`@tanstack/query-core`\"" || true
fi

# 4) Search for setQueryDefaults/setOptions usage
rg -n "setQueryDefaults|setOptions\(|QueryCache\." src -S || true

# 5) Locate definitions of getStaleTimeForKey / getGcTimeForKey
rg -n "getStaleTimeForKey|getGcTimeForKey" src/lib/query-config.ts -S || true

Repository: adm01-debug/promo-gifts-v4

Length of output: 8159


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm whether setQueryDefaults / setOptions exist anywhere in src
rg -n "setQueryDefaults\(|setOptions\(" src -S || true

# Look for tests mentioned in comments / likely naming
find . -maxdepth 4 -type f \( -name "*query-config*test*" -o -name "*query-config*.test.*" -o -name "*query-config*.spec.*" \) 2>/dev/null || true

# Search tests for getStaleTimeForKey / getGcTimeForKey
rg -n "getStaleTimeForKey|getGcTimeForKey|CACHE_TIMES|GC_TIMES" . -S --glob '*test*' --glob '*spec*' 2>/dev/null || true

Repository: adm01-debug/promo-gifts-v4

Length of output: 4798


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Search whole repo for setQueryDefaults / setOptions
rg -n "setQueryDefaults\(|setOptions\(" . -S --glob '*.{ts,tsx,js,jsx}' || true

# 2) Search whole repo for QueryCache subscribe usages
rg -n "getQueryCache\(\)\.subscribe|QueryCache.*subscribe" . -S --glob '*.{ts,tsx,js,jsx}' || true

# 3) Inspect createQueryClient behavior tests (if any mention subscribe/mutation)
sed -n '1,220p' tests/lib/query-config.test.ts | cat
echo "-----"
sed -n '1,220p' tests/lib/query-config-extended.test.ts | cat

Repository: adm01-debug/promo-gifts-v4

Length of output: 6111


🌐 Web query:

TanStack Query v5 QueryCache subscribe event.query.options mutate setOptions gcTime staleTime setQueryDefaults

💡 Result:

In TanStack Query v5, the items in your query map to these APIs/behaviors: 1) QueryCache.subscribe(event) Use queryClient.getQueryCache.subscribe(callback) to subscribe to query-cache updates as a whole. The callback receives a QueryCacheNotifyEvent and is called for “safe/known updates” such as query state changes, queries being updated, added, or removed [1]. Example (v5): const queryCache = queryClient.getQueryCache const unsubscribe = queryCache.subscribe((event) => { console.log(event.type, event.query) }) This subscribe method’s callback signature is callback: (event: QueryCacheNotifyEvent) => void and it returns an unsubscribe function [1]. 2) “event.query.options” The QueryCacheNotifyEvent includes a query object (event.query). In the official docs example, the event payload includes event.query (and you can log it), but the reference docs snippet doesn’t explicitly document the nested property name “options” on that query object [1]. So: the supported/officially documented part here is that event.query is present; accessing event.query.options is not directly confirmed by the cited docs pages we found. 3) Query.setOptions / mutate setOptions TanStack Query v5 does have query instance methods like query.setState (mentioned as a tracked update mechanism for firing queryCache.subscribe callbacks) [1], but the searched authoritative docs did not surface a specific “setOptions” method name or a “mutate setOptions” phrasing on QueryCache. What is confirmed is that cache subscriptions fire when updates happen via tracked mechanisms such as query.setState and queryClient.removeQueries [1]. 4) gcTime and staleTime - staleTime: controls how long cached data is considered fresh before it becomes stale. Default is staleTime: 0 (immediately stale) [2]. While data is fresh, it is read from cache without triggering refetches; after it becomes stale, it may be refetched based on other conditions [2]. - gcTime: controls how long inactive queries remain in memory before being garbage collected; default is 5 minutes. The v5 migration rename is cacheTime -> gcTime [3]. The caching guide explicitly states gcTime determines how long inactive queries remain in memory before being garbage collected and that the default is 5 minutes [4]. 5) setQueryDefaults To set default query options for matching query keys, use queryClient.setQueryDefaults(queryKey, options). Official docs show: queryClient.setQueryDefaults(['posts'], { queryFn: fetchPosts }) [5]. The QueryClient reference also documents getQueryDefaults/setQueryDefaults and notes that if multiple query defaults match a query key, they are merged in registration order (more generic to more specific so specific overrides generic) [5]. How to combine these in practice (common pattern) - Configure defaults once (staleTime/gcTime/queryFn) with setQueryDefaults. - Subscribe to query-cache updates with queryCache.subscribe to react to query changes globally. Authoritative references used above: - QueryCache.subscribe behavior and callback shape [1] - QueryClient.setQueryDefaults and merging/registration order [5] - staleTime defaults/meaning and gcTime default/meaning [2][4] - v5 migration cacheTime -> gcTime rename [3]

Citations:


Evite mutar query.options diretamente no client.getQueryCache().subscribe (src/lib/query-config.ts, ~linhas 220-228).

Essa abordagem depende de um detalhe não documentado da payload do QueryCache.subscribe (você acessa event.query.options e altera staleTime/gcTime via mutation). Preferir configurar defaults pelas chaves via API pública queryClient.setQueryDefaults(queryKey, options) (ex.: usando prefixos do queryKey) e deixar o fluxo interno aplicar staleTime/gcTime, reduzindo risco de desincronização/regressões. tanstack.com tanstack.com

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/query-config.ts` around lines 220 - 228, Don't mutate
event.query.options inside client.getQueryCache().subscribe; instead compute
defaults using getStaleTimeForKey and getGcTimeForKey and call the public API
queryClient.setQueryDefaults(query.queryKey, { staleTime, gcTime }) (or similar
on your `client` instance) so defaults are applied via the QueryClient flow;
remove the in-place assignments to query.options and replace them with
setQueryDefaults calls when event.type is 'added' or 'updated' so the library's
public mechanism manages defaults.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 2 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/lib/query-config.ts">

<violation number="1" location="src/lib/query-config.ts:226">
P2: Per-key `gcTime` routing is effectively skipped because `gcTime` is defaulted before the `undefined` check, so mapped prefixes keep the default retention.</violation>
</file>

<file name="src/contexts/ProductsContext.tsx">

<violation number="1" location="src/contexts/ProductsContext.tsx:27">
P3: `getProductById` documentation is incorrect: it says no fetch is triggered, but the function queues a fetch on cache miss.</violation>

<violation number="2" location="src/contexts/ProductsContext.tsx:114">
P1: Missing negative-cache guard causes infinite refetch loop for non-existent product IDs. When a requested ID isn't returned by `fetchPromobrindProducts` (e.g. deleted/inactive product), the `finally` block removes it from `fetchingRef` but it never enters `cache`. On the next render, `getProductsByIds`/`getProductById` will see the ID is absent from `cache`, `fetchingRef`, and `batchIdsRef`, and will re-queue it — repeating every 50 ms indefinitely. Track requested-but-not-returned IDs (e.g. in a `notFoundRef` set) and include them in the `queueFetch` filter to break the cycle.</violation>

<violation number="3" location="src/contexts/ProductsContext.tsx:115">
P2: `isLoading` can become false while requests are still in flight when batched fetches overlap.</violation>

<violation number="4" location="src/contexts/ProductsContext.tsx:143">
P2: `getProductsByIds` reads from `cacheRef.current` (a ref) but its identity never changes when `cache` state is updated because it only depends on `[queueFetch]`. Consumers that memoize results via `useMemo(() => getProductsByIds(ids), [getProductsByIds, ids])` will keep the stale empty array returned before the lazy fetch completed. Include a cache-derived signal (e.g. `cache` or `cache.size`) in the dependency array so the callback identity changes when new products arrive, or return the lookup directly from state rather than the ref.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

} catch (err) {
logger.warn('[ProductsContext] Failed to fetch products by IDs:', err);
} finally {
idsToFetch.forEach((id) => fetchingRef.current.delete(id));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Missing negative-cache guard causes infinite refetch loop for non-existent product IDs. When a requested ID isn't returned by fetchPromobrindProducts (e.g. deleted/inactive product), the finally block removes it from fetchingRef but it never enters cache. On the next render, getProductsByIds/getProductById will see the ID is absent from cache, fetchingRef, and batchIdsRef, and will re-queue it — repeating every 50 ms indefinitely. Track requested-but-not-returned IDs (e.g. in a notFoundRef set) and include them in the queueFetch filter to break the cycle.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/contexts/ProductsContext.tsx, line 114:

<comment>Missing negative-cache guard causes infinite refetch loop for non-existent product IDs. When a requested ID isn't returned by `fetchPromobrindProducts` (e.g. deleted/inactive product), the `finally` block removes it from `fetchingRef` but it never enters `cache`. On the next render, `getProductsByIds`/`getProductById` will see the ID is absent from `cache`, `fetchingRef`, and `batchIdsRef`, and will re-queue it — repeating every 50 ms indefinitely. Track requested-but-not-returned IDs (e.g. in a `notFoundRef` set) and include them in the `queueFetch` filter to break the cycle.</comment>

<file context>
@@ -24,90 +24,222 @@ interface ProductsContextType {
+      } catch (err) {
+        logger.warn('[ProductsContext] Failed to fetch products by IDs:', err);
+      } finally {
+        idsToFetch.forEach((id) => fetchingRef.current.delete(id));
+        if (mountedRef.current) setIsLoading(false);
+      }
</file context>

Comment thread src/lib/query-config.ts
query.options.staleTime = resolveStaleTime(query.queryKey);
query.options.staleTime = getStaleTimeForKey(query.queryKey);
}
if (query.options.gcTime === undefined) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Per-key gcTime routing is effectively skipped because gcTime is defaulted before the undefined check, so mapped prefixes keep the default retention.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/lib/query-config.ts, line 226:

<comment>Per-key `gcTime` routing is effectively skipped because `gcTime` is defaulted before the `undefined` check, so mapped prefixes keep the default retention.</comment>

<file context>
@@ -92,29 +208,28 @@ export const defaultQueryOptions: DefaultOptions = {
-        query.options.staleTime = resolveStaleTime(query.queryKey);
+        query.options.staleTime = getStaleTimeForKey(query.queryKey);
+      }
+      if (query.options.gcTime === undefined) {
+        query.options.gcTime = getGcTimeForKey(query.queryKey);
       }
</file context>

logger.warn('[ProductsContext] Failed to fetch products by IDs:', err);
} finally {
idsToFetch.forEach((id) => fetchingRef.current.delete(id));
if (mountedRef.current) setIsLoading(false);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: isLoading can become false while requests are still in flight when batched fetches overlap.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/contexts/ProductsContext.tsx, line 115:

<comment>`isLoading` can become false while requests are still in flight when batched fetches overlap.</comment>

<file context>
@@ -24,90 +24,222 @@ interface ProductsContextType {
+        logger.warn('[ProductsContext] Failed to fetch products by IDs:', err);
+      } finally {
+        idsToFetch.forEach((id) => fetchingRef.current.delete(id));
+        if (mountedRef.current) setIsLoading(false);
+      }
+    }, 50); // 50ms batching window
</file context>

}
return cached;
},
[queueFetch],
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: getProductsByIds reads from cacheRef.current (a ref) but its identity never changes when cache state is updated because it only depends on [queueFetch]. Consumers that memoize results via useMemo(() => getProductsByIds(ids), [getProductsByIds, ids]) will keep the stale empty array returned before the lazy fetch completed. Include a cache-derived signal (e.g. cache or cache.size) in the dependency array so the callback identity changes when new products arrive, or return the lookup directly from state rather than the ref.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/contexts/ProductsContext.tsx, line 143:

<comment>`getProductsByIds` reads from `cacheRef.current` (a ref) but its identity never changes when `cache` state is updated because it only depends on `[queueFetch]`. Consumers that memoize results via `useMemo(() => getProductsByIds(ids), [getProductsByIds, ids])` will keep the stale empty array returned before the lazy fetch completed. Include a cache-derived signal (e.g. `cache` or `cache.size`) in the dependency array so the callback identity changes when new products arrive, or return the lookup directly from state rather than the ref.</comment>

<file context>
@@ -24,90 +24,222 @@ interface ProductsContextType {
+      }
+      return cached;
+    },
+    [queueFetch],
+  );
+
</file context>
Suggested change
[queueFetch],
[queueFetch, cache],

fetchProducts: (ids: string[]) => Promise<void>;
getProduct: (id: string) => Product | undefined;
invalidateCache: () => void;
/** Resolves a single id (returns undefined if not cached; does NOT trigger fetch) */
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: getProductById documentation is incorrect: it says no fetch is triggered, but the function queues a fetch on cache miss.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/contexts/ProductsContext.tsx, line 27:

<comment>`getProductById` documentation is incorrect: it says no fetch is triggered, but the function queues a fetch on cache miss.</comment>

<file context>
@@ -24,90 +24,222 @@ interface ProductsContextType {
-  fetchProducts: (ids: string[]) => Promise<void>;
-  getProduct: (id: string) => Product | undefined;
-  invalidateCache: () => void;
+  /** Resolves a single id (returns undefined if not cached; does NOT trigger fetch) */
+  getProductById: (id: string) => Product | undefined;
+  /**
</file context>
Suggested change
/** Resolves a single id (returns undefined if not cached; does NOT trigger fetch) */
/** Resolves a single id (returns cached value when available; queues lazy fetch on miss) */

@adm01-debug adm01-debug closed this Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants