koala73 / koala73/worldmonitor
perf(markets): cache stablecoin gap lookups per coin, not per request shape
- Dominant language
- TypeScript
- Stars
- 86.6k
- Forks
- 13.1k
- Avg merge
- 8h 4m
- Merged PRs (30d)
- 825
Description
## Problem
#6308 gave `ListStablecoinMarkets` a bounded provider lookup for coin IDs the seeded snapshot does not carry. That lookup is cached under a key derived from a hash of the **whole request's gap-ID set**:
```
server/worldmonitor/market/v1/list-stablecoin-markets.ts
const cacheKey = `${GAP_CACHE_KEY_PREFIX}${await sha256Hex([...ids].sort().join(','))}`;
```
So reuse is per request *shape*, not per coin:
- `?coins=frax` → key = hash("frax")
- `?coins=frax,paxos-standard` → key = hash("frax,paxos-standard")
Both miss independently, and both fetch `frax` from CoinGecko — even a second apart. The same applies to negative results: an ID that does not exist re-triggers a provider call every time it appears in a different combination.
## Why it was left this way
Deliberate, and documented in the handler. Nothing sends a non-empty `coins` today — `src/components/StablecoinPanel.ts` sends `coins: []`, which is served from the snapshot alone and never reaches a provider. Per-coin keys would optimise a traffic pattern that does not exist yet, and they carry real complexity: the `PROVIDER_ERROR` vs `NOT_FOUND` split (`cacheFetcherErrors: false`) has to survive per-ID, so an outage must not be written as a per-coin negative.
Until a real caller with overlapping ID sets exists, the cost is bounded by the 25-ID cap and the endpoint's 60/min fail-closed rate policy.
## When to do this
When something actually starts sending overlapping `coins` sets — the deferred MCP tool in #4525 is the likely first one, since agent callers request arbitrary subsets.
## Sketch
1. Per-ID keys: `market:stablecoins:rpc:v1:`. IDs are already validated against `^[a-z0-9][a-z0-9-]{0,63}$` before reaching the lookup, so they are safe as raw key suffixes — no hashing needed.
2. Batch-read candidates with `getCachedJsonBatch` (single pipelined round-trip).
3. Fetch only the still-missing subset, still in one batched provider call.
4. Write each resolved coin back under its own key.
## Acceptance criteria
- [ ] `?coins=frax` followed by `?coins=frax,paxos-standard` makes exactly one provider call for `frax` — with a test, since the current cache-reuse test repeats an identical request and cannot catch this.
- [ ] A provider outage is still never written as a per-coin negative; `PROVIDER_ERROR` and `NOT_FOUND` stay distinguishable.
- [ ] The empty-`coins` request still performs zero provider calls under every Redis state.
## Related
- #6308 (introduced the lookup and this caching granularity)
- #4525 (the deferred MCP tool, the likely first caller to care)
Found by an efficiency review pass during #6308.
Contributor guide
Research direction
Start in server/worldmonitor/market/v1/list-stablecoin-markets.ts and inspect the existing cache-reuse test, getCachedJsonBatch, and provider error handling. Verify the empty-coins path and the current PROVIDER_ERROR/NOT_FOUND distinction before changing the cache flow. Done means overlapping requests fetch each coin only once, outages are not cached as negatives, and empty requests make zero provider calls.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- redis, typescript
- Domain
- api, backend, performance
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100