Skip to content

Cache /api/stellar/liquidity lookups (#375) - #552

Open
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Northgate-Systems:mainfrom
Vyacheslav-Tomashevskiy:fix/375-cache-stellar-liquidity-response
Open

Cache /api/stellar/liquidity lookups (#375)#552
Vyacheslav-Tomashevskiy wants to merge 1 commit into
Northgate-Systems:mainfrom
Vyacheslav-Tomashevskiy:fix/375-cache-stellar-liquidity-response

Conversation

@Vyacheslav-Tomashevskiy

Copy link
Copy Markdown
Contributor

Closes #375.

What was actually missing

Unlike #374's /api/stellar/rate (whose sibling rates.ts already had an in-memory cache, so that one just needed the HTTP layer), /api/stellar/liquidity had no caching anywhere. Every single request called server.liquidityPools().forAssets(asset).limit(1).order("desc").call() straight against Horizon - a real network round-trip, every time.

Change

  • src/lib/stellar.ts: new getLiquidityPool(assetCode) moves the lookup logic out of the route handler and wraps it with a short-TTL (60s) in-memory cache keyed by uppercased asset code - same pattern already used for rates in src/lib/rates.ts. The "no issuer configured" / "no pool found" answers get cached too: unlike rates.ts's hardcoded fallback rate (which is a degraded substitute for a live value and deliberately isn't cached that way), "this asset has no pool" is a stable, real answer that won't change until an issuer gets configured or a pool gets created - there's no reason to keep re-asking Horizon for it.
  • src/app/api/stellar/liquidity/route.ts: now calls getLiquidityPool() instead of querying Horizon directly, and sets Cache-Control on every response:
    • private, max-age=60, stale-while-revalidate=120 on a successful lookup - private (not public), since this route requires a session and a shared/CDN cache must never serve one user's authenticated response to a different, unauthenticated request.
    • no-store on the 401/400/500 paths, which weren't setting any Cache-Control before either.

Added src/lib/__tests__/stellar-liquidity-cache.test.ts (6 tests, mocking @stellar/stellar-sdk's Horizon.Server so no real network calls happen): a fresh asset hits Horizon once, a second lookup for the same asset is served from cache without a second Horizon call, different asset codes cache independently, an asset with no configured issuer never touches Horizon at all, that "no issuer" answer is cached too, and a genuinely empty pool result is reported with a reason. Each test imports a fresh copy of the module (vi.resetModules()) so the module-private cache doesn't leak state between tests.

Added src/app/api/stellar/liquidity/__tests__/route.test.ts (6 tests, mocking @/lib/auth and @/lib/stellar): correct Cache-Control on a live and a cached lookup, a null-pool answer still gets cached, and unauthenticated/missing-param/error paths all get no-store without ever calling getLiquidityPool.

Verification

  • Temporarily restored the known missing isValidStellarPublicKey import (fix(validations): restore broken isValidStellarPublicKey import; feat: add /api/stellar/fee-estimate #529) to run this locally, since validations.ts won't load on main without it. New test files: 12/12 passing. Reverted before this commit (git diff on validations.ts is empty in this PR).
  • Full suite: 76 passed (the pre-existing 64 plus these 12), same 2 pre-existing failures from a bad fixture address in validations.test.ts, unrelated to this change.
  • npm run dev + curl: unauthenticated and missing-param requests to the live route both return the expected status with Cache-Control: no-store, confirming the header logic runs on a real server. Couldn't drive the 200 path live in this sandbox (no real Supabase instance to back a session), so that path - and the actual caching behavior - is covered by the unit tests instead.
  • tsc --noEmit - no new errors (same pre-existing fix(validations): restore broken isValidStellarPublicKey import; feat: add /api/stellar/fee-estimate #529 and an unrelated transactions/[id] test type-mismatch present on main).
  • eslint - clean.

/claim

Unlike Northgate-Systems#374's /api/stellar/rate (whose sibling rates.ts already had an
in-memory cache), /api/stellar/liquidity had no caching anywhere - every
request called server.liquidityPools().forAssets(asset).limit(1).order("desc").call()
straight against Horizon, a real network round-trip, on every single call.

- src/lib/stellar.ts: new getLiquidityPool(assetCode) wraps the existing
  lookup logic (moved out of the route handler) with a short-TTL
  (60s) in-memory cache keyed by uppercased asset code, matching the
  pattern already used for rates in src/lib/rates.ts. The "no issuer
  configured" / "no pool found" answers are cached too - they're a
  stable, real result (not a degraded fallback the way rates.ts's
  hardcoded rate is), so there's no reason to re-ask Horizon for an
  asset that will never have a pool.
- src/app/api/stellar/liquidity/route.ts: now calls getLiquidityPool()
  instead of querying Horizon directly, and sets Cache-Control on every
  response - 'private, max-age=60, stale-while-revalidate=120' on a
  successful lookup (private, not public, since this route requires a
  session), 'no-store' on the 401/400/500 paths, which weren't setting
  any Cache-Control before either.
- Added src/lib/__tests__/stellar-liquidity-cache.test.ts (6 tests,
  mocking @stellar/stellar-sdk's Horizon.Server so no real network calls
  happen): a fresh asset hits Horizon once, a second lookup for the same
  asset is served from cache without a second Horizon call, different
  asset codes cache independently, an asset with no configured issuer
  never touches Horizon at all, that 'no issuer' answer is cached too,
  and a genuinely empty pool result is reported with a reason. Each test
  imports a fresh copy of the module (vi.resetModules()) so the
  module-private cache Map starts empty every time - otherwise leftover
  cache state from an earlier test would make the 'does this actually
  skip Horizon' assertions pass for the wrong reason.
- Added src/app/api/stellar/liquidity/__tests__/route.test.ts (6 tests,
  mocking @/lib/auth and @/lib/stellar): correct Cache-Control on a live
  and a cached lookup, a null-pool answer still gets cached (it's not a
  degraded value), unauthenticated/missing-param/error paths all get
  no-store and never call getLiquidityPool.

Verified: temporarily restored the known missing isValidStellarPublicKey
import (Northgate-Systems#529) to run this locally, since validations.ts (and therefore
this route, transitively) won't load on main without it - reverted before
this commit, git diff on validations.ts is empty.
- New test files: 12/12 passing. Full suite: 76 passed (the pre-existing
  64 plus these 12), same 2 pre-existing failures from a bad fixture
  address in validations.test.ts, unrelated to this change.
- npm run dev + curl: unauthenticated and missing-param requests to the
  live route both return the expected status with Cache-Control:
  no-store, confirming the header logic runs on a real server. Couldn't
  drive the 200 path live in this sandbox (no real Supabase instance to
  back a session), so that path - and the actual caching behavior - is
  covered by the unit tests instead.
- tsc --noEmit: no new errors (same pre-existing Northgate-Systems#529 and an unrelated
  transactions/[id] test type-mismatch present on main).
- eslint: clean.
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the codex723's projects Team on Vercel.

A member of the Team first needs to authorize it.

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.

Cache /api/stellar/liquidity responses

1 participant