-
Notifications
You must be signed in to change notification settings - Fork 457
Description
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 eachuseAgentcall 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
createCacheKeyso 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
onopenfires after a disconnect, clear cache entries for that agent+name. - WeakRef-based approach — store
WeakRefto promises so GC can reclaim unused entries.