Skip to content

queryCache in react.tsx is a module-level singleton — risks cross-instance bleed and unbounded growth #1025

@threepointone

Description

@threepointone

Problem

packages/agents/src/react.tsx:17 has a module-level singleton:

const queryCache = new Map<string, CacheEntry>();

This single Map is shared by every component that imports agents/react, which causes several issues:

1. Cross-instance cache bleed

The cache key is JSON.stringify([agentNamespace, name, ...deps]). If two useAgent hooks connect to the same agent namespace + name but with different auth tokens or user contexts, they share cache entries. A query result fetched with User A's token could be served from cache to User B's hook.

2. Stale entries survive HMR

During hot module reload in development, the module may be re-evaluated but the old Map instance can persist if the bundler preserves module state. Stale promises from a previous render cycle linger in the cache and get served to new hook instances.

3. Unbounded growth

Entries are only evicted by TTL expiry (expiresAt check in getCacheEntry). If you mount/unmount many hooks with distinct cache keys over time, the Map grows without bound. There is no LRU eviction, no max-size cap, and no cleanup on unmount.

4. No invalidation on reconnect

When a useAgent hook disconnects and reconnects (e.g., network flap, tab re-focus), the cache still serves old entries until TTL expires. The reconnected WebSocket may have fresh server state, but queries hit stale cache.

Possible Fixes

  • Scope the cache per hook instance — move the Map into the hook via useRef, so each useAgent call has its own cache. Eliminates cross-instance bleed but loses sharing.
  • Add an auth/context dimension to the key — include a token hash or user ID in createCacheKey so different auth contexts never collide.
  • Add a max-size cap + LRU — evict oldest entries when the Map exceeds a threshold.
  • Invalidate on reconnect — when the WebSocket onopen fires after a disconnect, clear cache entries for that agent+name.
  • WeakRef-based approach — store WeakRef to promises so GC can reclaim unused entries.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions