From c46df214dbf7efef0c22e9913ad35da56105326b Mon Sep 17 00:00:00 2001 From: matheus1lva Date: Mon, 16 Feb 2026 11:17:47 -0300 Subject: [PATCH 1/5] Consolidate REST cache into shared Keyv instance with batch writes Replace per-module Keyv factory functions with a single shared instance and optimize cache writes using batch setMany operations. - Add shared cache.ts with single Keyv client for all REST endpoints - Remove createKeyv factories from list, reports, snapshot, timeseries - Switch refresh scripts to batch setMany for fewer Redis round-trips - Consolidate timeseries into one key per vault (all labels together) - Remove manual JSON.stringify/parse in favor of Keyv serialization - Ensure keyv.disconnect() on refresh script exit Co-Authored-By: Claude --- packages/web/app/api/rest/cache.ts | 3 ++ packages/web/app/api/rest/list/redis.ts | 9 +--- packages/web/app/api/rest/list/refresh.ts | 23 ++++---- .../api/rest/list/vaults/[chainId]/route.ts | 20 ++----- .../web/app/api/rest/list/vaults/route.ts | 17 ++---- .../rest/reports/[chainId]/[address]/route.ts | 41 ++++++-------- packages/web/app/api/rest/reports/redis.ts | 7 --- packages/web/app/api/rest/reports/refresh.ts | 41 +++++++------- .../snapshot/[chainId]/[address]/route.ts | 13 ++--- packages/web/app/api/rest/snapshot/redis.ts | 7 --- .../app/api/rest/snapshot/refresh-snapshot.ts | 41 +++++++------- .../[segment]/[chainId]/[address]/route.ts | 18 +++---- packages/web/app/api/rest/timeseries/redis.ts | 10 +--- .../api/rest/timeseries/refresh-timeseries.ts | 54 ++++++++++--------- 14 files changed, 130 insertions(+), 174 deletions(-) create mode 100644 packages/web/app/api/rest/cache.ts diff --git a/packages/web/app/api/rest/cache.ts b/packages/web/app/api/rest/cache.ts new file mode 100644 index 00000000..89e6f44a --- /dev/null +++ b/packages/web/app/api/rest/cache.ts @@ -0,0 +1,3 @@ +import { createKeyv } from '@keyv/redis' + +export const keyv = createKeyv(process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379') diff --git a/packages/web/app/api/rest/list/redis.ts b/packages/web/app/api/rest/list/redis.ts index 3d9f41d1..1088681b 100644 --- a/packages/web/app/api/rest/list/redis.ts +++ b/packages/web/app/api/rest/list/redis.ts @@ -1,8 +1,3 @@ -import { createKeyv } from '@keyv/redis' - -export function createListsKeyv(namespace?: string) { - const redisUrl = process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379' - return createKeyv(redisUrl, { - namespace, - }) +export function getListKey(chainIdOrAll: string): string { + return `list:vaults:${chainIdOrAll}` } diff --git a/packages/web/app/api/rest/list/refresh.ts b/packages/web/app/api/rest/list/refresh.ts index 4bdeb94a..396e744f 100644 --- a/packages/web/app/api/rest/list/refresh.ts +++ b/packages/web/app/api/rest/list/refresh.ts @@ -1,11 +1,10 @@ import { getVaultsList } from './db' -import { createListsKeyv } from './redis' +import { getListKey } from './redis' +import { keyv } from '../cache' async function refresh(): Promise { console.time('refresh') - const keyv = createListsKeyv('list:vaults') - const vaults = await getVaultsList() const vaultsByChain = vaults.reduce((acc, vault) => { @@ -17,12 +16,14 @@ async function refresh(): Promise { }, {} as Record) const chainIds = Object.keys(vaultsByChain).map(Number) - for (const chainId of chainIds) { - const chainVaults = vaultsByChain[chainId] - await keyv.set(String(chainId), JSON.stringify(chainVaults)) - } + const entries = chainIds.map((chainId) => ({ + key: getListKey(String(chainId)), + value: vaultsByChain[chainId], + })) + + entries.push({ key: getListKey('all'), value: vaults }) - await keyv.set('all', JSON.stringify(vaults)) + await (keyv as any).setMany(entries) console.log(`✓ Completed: ${vaults.length} vaults cached across ${chainIds.length} chains`) console.timeEnd('refresh') @@ -30,11 +31,13 @@ async function refresh(): Promise { if (require.main === module) { refresh() - .then(() => { + .then(async () => { + await keyv.disconnect() process.exit(0) }) - .catch(err => { + .catch(async (err) => { console.error(err) + await keyv.disconnect() process.exit(1) }) } diff --git a/packages/web/app/api/rest/list/vaults/[chainId]/route.ts b/packages/web/app/api/rest/list/vaults/[chainId]/route.ts index 96fd7a99..c1d3d2da 100644 --- a/packages/web/app/api/rest/list/vaults/[chainId]/route.ts +++ b/packages/web/app/api/rest/list/vaults/[chainId]/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from 'next/server' -import { createListsKeyv } from '../../redis' - +import { getListKey } from '../../redis' +import { keyv } from '../../../cache' import type { VaultListItem } from '../../db' export const runtime = 'nodejs' @@ -10,8 +10,6 @@ const corsHeaders = { 'access-control-allow-methods': 'GET,OPTIONS', } -const listsKeyv = createListsKeyv('list:vaults') - type RouteParams = { chainId?: string | string[] } @@ -30,26 +28,18 @@ export async function GET( return new NextResponse('Invalid chainId', { status: 400, headers: corsHeaders }) } - let cached + let vaults: VaultListItem[] | undefined try { - cached = await listsKeyv.get(String(chainId)) + vaults = await keyv.get(getListKey(String(chainId))) as VaultListItem[] | undefined } catch (err) { console.error(`Redis read failed for chainId ${chainId}:`, err) throw err } - if (!cached) { + if (!vaults) { return new NextResponse('Not found', { status: 404, headers: corsHeaders }) } - let vaults: VaultListItem[] - try { - vaults = JSON.parse(cached as string) - } catch (e) { - console.error(`Failed to parse vault list for chain ${chainId}:`, e) - return new NextResponse('Internal Server Error', { status: 500, headers: corsHeaders }) - } - const filtered = origin ? vaults.filter(v => v.origin === origin) : vaults diff --git a/packages/web/app/api/rest/list/vaults/route.ts b/packages/web/app/api/rest/list/vaults/route.ts index e2e0ec93..bbe2dd8f 100644 --- a/packages/web/app/api/rest/list/vaults/route.ts +++ b/packages/web/app/api/rest/list/vaults/route.ts @@ -1,5 +1,6 @@ import { NextResponse } from 'next/server' -import { createListsKeyv } from '../redis' +import { getListKey } from '../redis' +import { keyv } from '../../cache' import type { VaultListItem } from '../db' export const runtime = 'nodejs' @@ -9,27 +10,17 @@ const corsHeaders = { 'access-control-allow-methods': 'GET,OPTIONS', } -const listsKeyv = createListsKeyv('list:vaults') - export async function GET(request: Request) { const { searchParams } = new URL(request.url) const origin = searchParams.get('origin') try { - const cached = await listsKeyv.get('all') + const allVaults = await keyv.get(getListKey('all')) as VaultListItem[] | undefined - if (!cached) { + if (!allVaults) { return new NextResponse('Not found', { status: 404, headers: corsHeaders }) } - let allVaults: VaultListItem[] - try { - allVaults = JSON.parse(cached as string) - } catch (e) { - console.error('Failed to parse vault list from Redis:', e) - return new NextResponse('Internal Server Error', { status: 500, headers: corsHeaders }) - } - const filtered = origin ? allVaults.filter(v => v.origin === origin) : allVaults diff --git a/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts b/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts index 8e038cfc..a347a251 100644 --- a/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts @@ -1,7 +1,13 @@ import { NextRequest, NextResponse } from 'next/server' -import { createReportsKeyv, getReportKey} from '../../redis' +import { getReportKey } from '../../redis' +import { keyv } from '../../../cache' import { VaultReport } from '../../db' +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, OPTIONS', +} + export async function GET( request: NextRequest, { params }: { params: Promise<{ chainId: string; address: string }> } @@ -17,43 +23,28 @@ export async function GET( ) } - const keyv = createReportsKeyv() const key = getReportKey(chainId, address) + const data = await keyv.get(key) as VaultReport[] | undefined - const cached = await keyv.get(key) - - if (!cached) { + if (!data) { return NextResponse.json( { error: 'Not found' }, { status: 404 } ) } - let data - try { - data = typeof cached === 'string' ? JSON.parse(cached) : cached - - return NextResponse.json(data, { - headers: { - 'Cache-Control': 'public, max-age=900, s-maxage=900, stale-while-revalidate=600', - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS' - } - }) - } catch (e) { - console.error('Failed to parse cached report', e) - return NextResponse.json( - { error: 'Internal Server Error' }, - { status: 500 } - ) - } + return NextResponse.json(data, { + headers: { + 'Cache-Control': 'public, max-age=900, s-maxage=900, stale-while-revalidate=600', + ...corsHeaders, + } + }) } export async function OPTIONS() { return new NextResponse(null, { headers: { - 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET, OPTIONS', + ...corsHeaders, 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }, }) diff --git a/packages/web/app/api/rest/reports/redis.ts b/packages/web/app/api/rest/reports/redis.ts index 90318c6d..05d03fb7 100644 --- a/packages/web/app/api/rest/reports/redis.ts +++ b/packages/web/app/api/rest/reports/redis.ts @@ -1,10 +1,3 @@ -import { createKeyv } from '@keyv/redis' - -export function createReportsKeyv() { - const redisUrl = process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379' - return createKeyv(redisUrl) -} - export function getReportKey( chainId: number, address: string, diff --git a/packages/web/app/api/rest/reports/refresh.ts b/packages/web/app/api/rest/reports/refresh.ts index f26e1337..59970c60 100644 --- a/packages/web/app/api/rest/reports/refresh.ts +++ b/packages/web/app/api/rest/reports/refresh.ts @@ -1,12 +1,12 @@ import 'lib/global' import { getVaults, getStrategyReports } from './db' -import { createReportsKeyv, getReportKey } from './redis' +import { getReportKey } from './redis' +import { keyv } from '../cache' const BATCH_SIZE = parseInt(process.env.REFRESH_BATCH_SIZE || '10', 10) async function refreshReports(): Promise { console.time('refreshReports') - const keyv = createReportsKeyv() console.log('Fetching vaults...') const vaults = await getVaults() @@ -14,40 +14,41 @@ async function refreshReports(): Promise { let processed = 0 - async function processVault(vault: { chainId: number; address: string }) { - const addressLower = vault.address.toLowerCase() - - const reports = await getStrategyReports(vault.chainId, vault.address) - - if (!reports || reports.length === 0) { - return + for (let i = 0; i < vaults.length; i += BATCH_SIZE) { + const batch = vaults.slice(i, i + BATCH_SIZE) + const results = await Promise.all(batch.map(async (vault) => { + const reports = await getStrategyReports(vault.chainId, vault.address) + if (!reports || reports.length === 0) return null + return { + key: getReportKey(vault.chainId, vault.address.toLowerCase()), + value: reports, + } + })) + + const entries = results.filter((r): r is NonNullable => r !== null) + if (entries.length > 0) { + await (keyv as any).setMany(entries) } - const cacheKey = getReportKey(vault.chainId, addressLower) - await keyv.set(cacheKey, JSON.stringify(reports)) - - processed++ + processed += entries.length if (processed % 10 === 0) { console.log(`Processed ${processed}/${vaults.length} vaults`) } } - for (let i = 0; i < vaults.length; i += BATCH_SIZE) { - const batch = vaults.slice(i, i + BATCH_SIZE) - await Promise.all(batch.map(processVault)) - } - console.log(`✓ Completed: ${processed} vaults processed`) console.timeEnd('refreshReports') } if (require.main === module) { refreshReports() - .then(() => { + .then(async () => { + await keyv.disconnect() process.exit(0) }) - .catch(err => { + .catch(async (err) => { console.error(err) + await keyv.disconnect() process.exit(1) }) } diff --git a/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts b/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts index 07c95051..8b410573 100644 --- a/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts @@ -1,5 +1,6 @@ import { NextRequest, NextResponse } from 'next/server' -import { createSnapshotKeyv, getSnapshotKey } from '../../redis' +import { getSnapshotKey } from '../../redis' +import { keyv } from '../../../cache' import type { VaultSnapshot } from '../../db' export const runtime = 'nodejs' @@ -14,8 +15,6 @@ const corsHeaders = { 'access-control-allow-methods': 'GET,OPTIONS', } -const snapshotKeyv = createSnapshotKeyv() - export async function GET( request: NextRequest, context: { params: Promise }, @@ -31,20 +30,18 @@ export async function GET( const addressLower = address.toLowerCase() const cacheKey = getSnapshotKey(Number(chainId), addressLower) - let cached: string | undefined + let parsed: VaultSnapshot | undefined try { - cached = await snapshotKeyv.get(cacheKey) + parsed = await keyv.get(cacheKey) } catch (err) { console.error(`Redis read failed for ${cacheKey}:`, err) throw err } - if (!cached) { + if (!parsed) { return new NextResponse('Not found', { status: 404, headers: corsHeaders }) } - const parsed: VaultSnapshot = JSON.parse(cached as string) - return NextResponse.json(parsed, { status: 200, headers: { diff --git a/packages/web/app/api/rest/snapshot/redis.ts b/packages/web/app/api/rest/snapshot/redis.ts index 4ad3ae97..c9f48f34 100644 --- a/packages/web/app/api/rest/snapshot/redis.ts +++ b/packages/web/app/api/rest/snapshot/redis.ts @@ -1,10 +1,3 @@ -import { createKeyv } from '@keyv/redis' - -export function createSnapshotKeyv() { - const redisUrl = process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379' - return createKeyv(redisUrl) -} - export function getSnapshotKey( chainId: number, address: string, diff --git a/packages/web/app/api/rest/snapshot/refresh-snapshot.ts b/packages/web/app/api/rest/snapshot/refresh-snapshot.ts index 64a9a8dc..b654f49e 100644 --- a/packages/web/app/api/rest/snapshot/refresh-snapshot.ts +++ b/packages/web/app/api/rest/snapshot/refresh-snapshot.ts @@ -1,11 +1,11 @@ import { getVaults, getVaultSnapshot } from './db' -import { createSnapshotKeyv, getSnapshotKey } from './redis' +import { getSnapshotKey } from './redis' +import { keyv } from '../cache' const BATCH_SIZE = 10 async function refresh(): Promise { console.time('refresh') - const keyv = createSnapshotKeyv() console.log('Fetching vaults...') const vaults = await getVaults() @@ -13,40 +13,41 @@ async function refresh(): Promise { let processed = 0 - async function processVault(vault: { chainId: number; address: string }) { - const addressLower = vault.address.toLowerCase() - - const snapshot = await getVaultSnapshot(vault.chainId, vault.address) - - if (!snapshot) { - return + for (let i = 0; i < vaults.length; i += BATCH_SIZE) { + const batch = vaults.slice(i, i + BATCH_SIZE) + const snapshots = await Promise.all(batch.map(async (vault) => { + const snapshot = await getVaultSnapshot(vault.chainId, vault.address) + if (!snapshot) return null + return { + key: getSnapshotKey(vault.chainId, vault.address.toLowerCase()), + value: snapshot, + } + })) + + const entries = snapshots.filter((s): s is NonNullable => s !== null) + if (entries.length > 0) { + await (keyv as any).setMany(entries) } - const cacheKey = getSnapshotKey(vault.chainId, addressLower) - await keyv.set(cacheKey, JSON.stringify(snapshot)) - - processed++ + processed += entries.length if (processed % 10 === 0) { console.log(`Processed ${processed}/${vaults.length} vaults`) } } - for (let i = 0; i < vaults.length; i += BATCH_SIZE) { - const batch = vaults.slice(i, i + BATCH_SIZE) - await Promise.all(batch.map(processVault)) - } - console.log(`✓ Completed: ${processed} vaults processed`) console.timeEnd('refresh') } if (require.main === module) { refresh() - .then(() => { + .then(async () => { + await keyv.disconnect() process.exit(0) }) - .catch(err => { + .catch(async (err) => { console.error(err) + await keyv.disconnect() process.exit(1) }) } diff --git a/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts b/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts index 0714fb5f..44fad190 100644 --- a/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' import { labels } from '../../../labels' -import { createTimeseriesKeyv, getTimeseriesKey } from '../../../redis' +import { getTimeseriesKey } from '../../../redis' +import { keyv } from '../../../../cache' export const runtime = 'nodejs' @@ -10,13 +11,14 @@ type RouteParams = { address?: string | string[] } +type TimeseriesEntry = { time: number; component: string; value: number } +type ConsolidatedTimeseries = Record + const corsHeaders = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,OPTIONS', } -const timeseriesKeyv = createTimeseriesKeyv() - export async function GET( request: NextRequest, context: { params: Promise }, @@ -44,18 +46,16 @@ export async function GET( : [entry.defaultComponent] const addressLower = address.toLowerCase() - const cacheKey = getTimeseriesKey(entry.label, Number(chainId), addressLower) - let cached + const cacheKey = getTimeseriesKey(Number(chainId), addressLower) + let consolidated: ConsolidatedTimeseries | undefined try { - cached = await timeseriesKeyv.get(cacheKey) + consolidated = await keyv.get(cacheKey) } catch (err) { console.error(`Redis read failed for ${cacheKey}:`, err) throw err } - const parsed: Array<{ time: number; component: string; value: number }> = cached - ? JSON.parse(cached as string) - : [] + const parsed = consolidated?.[entry.label] ?? [] const filtered = parsed.filter((row) => components.includes(row.component)) return NextResponse.json(filtered, { diff --git a/packages/web/app/api/rest/timeseries/redis.ts b/packages/web/app/api/rest/timeseries/redis.ts index ebca8a45..2ef3303c 100644 --- a/packages/web/app/api/rest/timeseries/redis.ts +++ b/packages/web/app/api/rest/timeseries/redis.ts @@ -1,14 +1,6 @@ -import { createKeyv } from '@keyv/redis' - -export function createTimeseriesKeyv() { - const redisUrl = process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379' - return createKeyv(redisUrl) -} - export function getTimeseriesKey( - label: string, chainId: number, addressLower: string, ): string { - return `timeseries:${label}:${chainId}:${addressLower.toLowerCase()}` + return `timeseries:${chainId}:${addressLower.toLowerCase()}` } diff --git a/packages/web/app/api/rest/timeseries/refresh-timeseries.ts b/packages/web/app/api/rest/timeseries/refresh-timeseries.ts index 1e83c86a..df8eccd4 100644 --- a/packages/web/app/api/rest/timeseries/refresh-timeseries.ts +++ b/packages/web/app/api/rest/timeseries/refresh-timeseries.ts @@ -1,12 +1,12 @@ import { labels } from './labels' import { getFullTimeseries, getVaults, TimeseriesRow } from './db' -import { createTimeseriesKeyv, getTimeseriesKey } from './redis' +import { getTimeseriesKey } from './redis' +import { keyv } from '../cache' const BATCH_SIZE = 10 async function refresh24hr(): Promise { console.time('refresh24hr') - const keyv = createTimeseriesKeyv() console.log('Fetching vaults...') const vaults = await getVaults() @@ -14,48 +14,54 @@ async function refresh24hr(): Promise { let processed = 0 - async function processVault(vault: { chainId: number; address: string }) { - const addressLower = vault.address.toLowerCase() + for (let i = 0; i < vaults.length; i += BATCH_SIZE) { + const batch = vaults.slice(i, i + BATCH_SIZE) + const results = await Promise.all(batch.map(async (vault) => { + const addressLower = vault.address.toLowerCase() - await Promise.all(labels.map(async ({ label }) => { - const rows: TimeseriesRow[] = await getFullTimeseries( - vault.chainId, - vault.address, - label, - ) + const labelData: Record> = {} - const minimal = rows.map(row => ({ - time: Number(row.time), - component: row.component, - value: row.value, + await Promise.all(labels.map(async ({ label }) => { + const rows: TimeseriesRow[] = await getFullTimeseries( + vault.chainId, + vault.address, + label, + ) + + labelData[label] = rows.map(row => ({ + time: Number(row.time), + component: row.component, + value: row.value, + })) })) - const cacheKey = getTimeseriesKey(label, vault.chainId, addressLower) - await keyv.set(cacheKey, JSON.stringify(minimal)) + return { + key: getTimeseriesKey(vault.chainId, addressLower), + value: labelData, + } })) - processed++ + await (keyv as any).setMany(results) + + processed += results.length if (processed % 10 === 0) { console.log(`Processed ${processed}/${vaults.length} vaults`) } } - for (let i = 0; i < vaults.length; i += BATCH_SIZE) { - const batch = vaults.slice(i, i + BATCH_SIZE) - await Promise.all(batch.map(processVault)) - } - console.log(`✓ Completed: ${processed} vaults processed`) console.timeEnd('refresh24hr') } if (require.main === module) { refresh24hr() - .then(() => { + .then(async () => { + await keyv.disconnect() process.exit(0) }) - .catch(err => { + .catch(async (err) => { console.error(err) + await keyv.disconnect() process.exit(1) }) } From 3a2dc3d2cae5a8b333582ba440555d04fcc13af0 Mon Sep 17 00:00:00 2001 From: matheus1lva Date: Wed, 18 Feb 2026 16:30:23 -0300 Subject: [PATCH 2/5] fix --- bun.lock | 21 +++++++++----- packages/web/app/api/db/index.ts | 2 +- packages/web/app/api/rest/cache.ts | 8 +++++- packages/web/app/api/rest/list/redis.ts | 3 -- packages/web/app/api/rest/list/refresh.ts | 13 +++++---- .../api/rest/list/vaults/[chainId]/route.ts | 7 +++-- .../web/app/api/rest/list/vaults/route.ts | 5 ++-- .../rest/reports/[chainId]/[address]/route.ts | 6 ++-- packages/web/app/api/rest/reports/refresh.ts | 12 ++++---- .../snapshot/[chainId]/[address]/route.ts | 6 ++-- .../app/api/rest/snapshot/refresh-snapshot.ts | 6 ++-- .../[segment]/[chainId]/[address]/route.ts | 17 +++++------ packages/web/app/api/rest/timeseries/redis.ts | 3 +- .../api/rest/timeseries/refresh-timeseries.ts | 28 ++++++++++--------- packages/web/package.json | 4 +-- 15 files changed, 83 insertions(+), 58 deletions(-) delete mode 100644 packages/web/app/api/rest/list/redis.ts diff --git a/bun.lock b/bun.lock index aa17d5b0..ad0c8e99 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "kong", @@ -136,9 +137,9 @@ "version": "0.1.0", "dependencies": { "@apollo/server": "^4.9.5", - "@apollo/utils.keyvadapter": "^3.1.0", + "@apollo/utils.keyvadapter": "4.0.1", "@as-integrations/next": "^3.0.0", - "@keyv/redis": "^4.4.1", + "@keyv/redis": "5.1.6", "autoprefixer": "10.4.15", "bullmq": "^5.21.2", "chart.js": "^4.4.0", @@ -203,7 +204,7 @@ "@apollo/utils.isnodelike": ["@apollo/utils.isnodelike@2.0.1", "", {}, "sha512-w41XyepR+jBEuVpoRM715N2ZD0xMD413UiJx8w5xnAZD2ZkSJnMJBoIzauK83kJpSgNuR6ywbV29jG9NmxjK0Q=="], - "@apollo/utils.keyvadapter": ["@apollo/utils.keyvadapter@3.1.0", "", { "dependencies": { "@apollo/utils.keyvaluecache": "^3.1.0", "dataloader": "^2.1.0", "keyv": "^4.4.0" } }, "sha512-q41MxH2gKwvXL28hUEfQHdSQ0F4u8KrPHUbvN/IkA7vh+KTRj0tG2eWLu42c438jjoV7hUhY1Ru/Dwq8zndEqg=="], + "@apollo/utils.keyvadapter": ["@apollo/utils.keyvadapter@4.0.1", "", { "dependencies": { "@apollo/utils.keyvaluecache": "^4.0.0", "dataloader": "^2.1.0", "keyv": "^5.1.0" } }, "sha512-nCsAM/uNi17sta6f98FOBiQc8qwx3As78LfUGmHWzaKnwpolzeV8Phz04W723CSaDGxeo/gxEkkQiK0oAdvGxw=="], "@apollo/utils.keyvaluecache": ["@apollo/utils.keyvaluecache@2.1.1", "", { "dependencies": { "@apollo/utils.logger": "^2.0.1", "lru-cache": "^7.14.1" } }, "sha512-qVo5PvUUMD8oB9oYvq4ViCjYAMWnZ5zZwEjNF37L2m1u528x5mueMlU+Cr1UinupCgdB78g+egA1G98rbJ03Vw=="], @@ -437,7 +438,7 @@ "@json-rpc-tools/utils": ["@json-rpc-tools/utils@1.7.6", "", { "dependencies": { "@json-rpc-tools/types": "^1.7.6", "@pedrouid/environment": "^1.0.1" } }, "sha512-HjA8x/U/Q78HRRe19yh8HVKoZ+Iaoo3YZjakJYxR+rw52NHo6jM+VE9b8+7ygkCFXl/EHID5wh/MkXaE/jGyYw=="], - "@keyv/redis": ["@keyv/redis@4.4.1", "", { "dependencies": { "@redis/client": "^1.6.0", "cluster-key-slot": "^1.1.2" }, "peerDependencies": { "keyv": "^5.3.4" } }, "sha512-ALRB/prv0ZQW+m20EaO9f9Jduzakd2edBhfq/Ro/T/AA6RQ4bn3FYNJKSORgPFgn19tWCYdivovSQgSsxkxufg=="], + "@keyv/redis": ["@keyv/redis@5.1.6", "", { "dependencies": { "@redis/client": "^5.10.0", "cluster-key-slot": "^1.1.2", "hookified": "^1.13.0" }, "peerDependencies": { "keyv": "^5.6.0" } }, "sha512-eKvW6pspvVaU5dxigaIDZr635/Uw6urTXL3gNbY9WTR8d3QigZQT+r8gxYSEOsw4+1cCBsC4s7T2ptR0WC9LfQ=="], "@keyv/serialize": ["@keyv/serialize@1.1.1", "", {}, "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA=="], @@ -1275,6 +1276,8 @@ "hoek": ["hoek@4.3.1", "", {}, "sha512-v7E+yIjcHECn973i0xHm4kJkEpv3C8sbYS4344WXbzYqRyiDD7rjnnKo4hsJkejQBAFdRMUGNHySeSPKSH9Rqw=="], + "hookified": ["hookified@1.15.1", "", {}, "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg=="], + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], "hyphenate-style-name": ["hyphenate-style-name@1.1.0", "", {}, "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw=="], @@ -1417,7 +1420,7 @@ "keccak": ["keccak@3.0.4", "", { "dependencies": { "node-addon-api": "^2.0.0", "node-gyp-build": "^4.2.0", "readable-stream": "^3.6.0" } }, "sha512-3vKuW0jV8J3XNTzvfyicFR5qvxrSAGl7KIhvgOu5cmWwM7tZRj3fMbj/pfIf4be7aznbc+prBWGjywox/g2Y6Q=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], "keyvaluestorage-interface": ["keyvaluestorage-interface@1.0.0", "", {}, "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g=="], @@ -2063,7 +2066,7 @@ "@apollo/server/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "@apollo/utils.keyvadapter/@apollo/utils.keyvaluecache": ["@apollo/utils.keyvaluecache@3.1.0", "", { "dependencies": { "@apollo/utils.logger": "^3.0.0", "lru-cache": "^10.0.0" } }, "sha512-MM/DKIqpQQbuNG1gNPAlGc45THdWkroTmN8o/J09merFwf/LlZ7+lAfcHFDXIYIknwKmUjJrOMS3OxYbjrz2hA=="], + "@apollo/utils.keyvadapter/@apollo/utils.keyvaluecache": ["@apollo/utils.keyvaluecache@4.0.0", "", { "dependencies": { "@apollo/utils.logger": "^3.0.0", "lru-cache": "^11.0.0" } }, "sha512-mKw1myRUkQsGPNB+9bglAuhviodJ2L2MRYLTafCMw5BIo7nbvCPNCkLnIHjZ1NOzH7SnMAr5c9LmXiqsgYqLZw=="], "@apollo/utils.keyvaluecache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], @@ -2097,7 +2100,7 @@ "@json-rpc-tools/provider/ws": ["ws@7.5.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-kQ/dHIzuLrS6Je9+uv81ueZomEwH0qVYstcAQ4/Z93K8zeko9gtAbttJWzoC5ukqXY1PpoouV3+VSOqEAFt5wg=="], - "@keyv/redis/keyv": ["keyv@5.5.5", "", { "dependencies": { "@keyv/serialize": "^1.1.1" } }, "sha512-FA5LmZVF1VziNc0bIdCSA1IoSVnDCqE8HJIZZv2/W8YmoAM50+tnUgJR/gQZwEeIMleuIOnRnHA/UaZRNeV4iQ=="], + "@keyv/redis/@redis/client": ["@redis/client@5.11.0", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0" }, "optionalPeers": ["@node-rs/xxhash"] }, "sha512-GHoprlNQD51Xq2Ztd94HHV94MdFZQ3CVrpA04Fz8MVoHM0B7SlbmPEVIjwTbcv58z8QyjnrOuikS0rWF03k5dQ=="], "@metamask/json-rpc-engine/@metamask/utils": ["@metamask/utils@8.5.0", "", { "dependencies": { "@ethereumjs/tx": "^4.2.0", "@metamask/superstruct": "^3.0.0", "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.3", "@types/debug": "^4.1.7", "debug": "^4.3.4", "pony-cause": "^2.1.10", "semver": "^7.5.4", "uuid": "^9.0.1" } }, "sha512-I6bkduevXb72TIM9q2LRO63JSsF9EXduh3sBr9oybNX2hNNpr/j1tEjXrsG0Uabm4MJ1xkGAQEMwifvKZIkyxQ=="], @@ -2215,6 +2218,8 @@ "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + "flat-cache/keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], @@ -2299,6 +2304,8 @@ "@apollo/utils.keyvadapter/@apollo/utils.keyvaluecache/@apollo/utils.logger": ["@apollo/utils.logger@3.0.0", "", {}, "sha512-M8V8JOTH0F2qEi+ktPfw4RL7MvUycDfKp7aEap2eWXfL5SqWHN6jTLbj5f5fj1cceHpyaUSOZlvlaaryaxZAmg=="], + "@apollo/utils.keyvadapter/@apollo/utils.keyvaluecache/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="], diff --git a/packages/web/app/api/db/index.ts b/packages/web/app/api/db/index.ts index c62d6d00..cac3cae9 100644 --- a/packages/web/app/api/db/index.ts +++ b/packages/web/app/api/db/index.ts @@ -14,7 +14,7 @@ const db = new Pool({ password: process.env.POSTGRES_PASSWORD ?? 'password', max: parseInt(process.env.POSTGRES_POOL_MAX ?? '4', 10), idleTimeoutMillis: 60_000, - connectionTimeoutMillis: 5_000, + connectionTimeoutMillis: 50_000, }) export default db diff --git a/packages/web/app/api/rest/cache.ts b/packages/web/app/api/rest/cache.ts index 89e6f44a..dad42f97 100644 --- a/packages/web/app/api/rest/cache.ts +++ b/packages/web/app/api/rest/cache.ts @@ -1,3 +1,9 @@ import { createKeyv } from '@keyv/redis' -export const keyv = createKeyv(process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379') +export function createKeyvClient(namespace?: string) { + const redisUrl = process.env.REST_CACHE_REDIS_URL || 'redis://localhost:6379' + return createKeyv(redisUrl, { + namespace, + }) +} + diff --git a/packages/web/app/api/rest/list/redis.ts b/packages/web/app/api/rest/list/redis.ts deleted file mode 100644 index 1088681b..00000000 --- a/packages/web/app/api/rest/list/redis.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function getListKey(chainIdOrAll: string): string { - return `list:vaults:${chainIdOrAll}` -} diff --git a/packages/web/app/api/rest/list/refresh.ts b/packages/web/app/api/rest/list/refresh.ts index 396e744f..8d963e9c 100644 --- a/packages/web/app/api/rest/list/refresh.ts +++ b/packages/web/app/api/rest/list/refresh.ts @@ -1,9 +1,10 @@ +import { createKeyvClient } from '../cache' import { getVaultsList } from './db' -import { getListKey } from './redis' -import { keyv } from '../cache' + +const keyv = createKeyvClient('list:vaults') async function refresh(): Promise { - console.time('refresh') + console.time('refresh list:vaults') const vaults = await getVaultsList() @@ -17,13 +18,13 @@ async function refresh(): Promise { const chainIds = Object.keys(vaultsByChain).map(Number) const entries = chainIds.map((chainId) => ({ - key: getListKey(String(chainId)), + key: String(chainId), value: vaultsByChain[chainId], })) - entries.push({ key: getListKey('all'), value: vaults }) + entries.push({ key: 'all', value: vaults }) - await (keyv as any).setMany(entries) + await keyv.setMany(entries) console.log(`✓ Completed: ${vaults.length} vaults cached across ${chainIds.length} chains`) console.timeEnd('refresh') diff --git a/packages/web/app/api/rest/list/vaults/[chainId]/route.ts b/packages/web/app/api/rest/list/vaults/[chainId]/route.ts index c1d3d2da..0d9370ac 100644 --- a/packages/web/app/api/rest/list/vaults/[chainId]/route.ts +++ b/packages/web/app/api/rest/list/vaults/[chainId]/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from 'next/server' -import { getListKey } from '../../redis' -import { keyv } from '../../../cache' +import { createKeyvClient } from '../../../cache' import type { VaultListItem } from '../../db' +const keyv = createKeyvClient('list:vaults') + export const runtime = 'nodejs' const corsHeaders = { @@ -30,7 +31,7 @@ export async function GET( let vaults: VaultListItem[] | undefined try { - vaults = await keyv.get(getListKey(String(chainId))) as VaultListItem[] | undefined + vaults = await keyv.get(String(chainId)) as VaultListItem[] | undefined } catch (err) { console.error(`Redis read failed for chainId ${chainId}:`, err) throw err diff --git a/packages/web/app/api/rest/list/vaults/route.ts b/packages/web/app/api/rest/list/vaults/route.ts index bbe2dd8f..291ef432 100644 --- a/packages/web/app/api/rest/list/vaults/route.ts +++ b/packages/web/app/api/rest/list/vaults/route.ts @@ -1,8 +1,9 @@ import { NextResponse } from 'next/server' -import { getListKey } from '../redis' -import { keyv } from '../../cache' +import { createKeyvClient } from '../../cache' import type { VaultListItem } from '../db' +const keyv = createKeyvClient('list:vaults') + export const runtime = 'nodejs' const corsHeaders = { diff --git a/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts b/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts index a347a251..191039c8 100644 --- a/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/reports/[chainId]/[address]/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' -import { getReportKey } from '../../redis' -import { keyv } from '../../../cache' +import { createKeyvClient } from '../../../cache' import { VaultReport } from '../../db' +import { getReportKey } from '../../redis' + +const keyv = createKeyvClient() const corsHeaders = { 'Access-Control-Allow-Origin': '*', diff --git a/packages/web/app/api/rest/reports/refresh.ts b/packages/web/app/api/rest/reports/refresh.ts index 59970c60..23a7e1a1 100644 --- a/packages/web/app/api/rest/reports/refresh.ts +++ b/packages/web/app/api/rest/reports/refresh.ts @@ -1,12 +1,14 @@ import 'lib/global' -import { getVaults, getStrategyReports } from './db' +import { createKeyvClient } from '../cache' +import { getStrategyReports, getVaults } from './db' import { getReportKey } from './redis' -import { keyv } from '../cache' + +const keyv = createKeyvClient() const BATCH_SIZE = parseInt(process.env.REFRESH_BATCH_SIZE || '10', 10) async function refreshReports(): Promise { - console.time('refreshReports') + console.time('refresh vault_reports') console.log('Fetching vaults...') const vaults = await getVaults() @@ -27,7 +29,7 @@ async function refreshReports(): Promise { const entries = results.filter((r): r is NonNullable => r !== null) if (entries.length > 0) { - await (keyv as any).setMany(entries) + await keyv.setMany(entries) } processed += entries.length @@ -37,7 +39,7 @@ async function refreshReports(): Promise { } console.log(`✓ Completed: ${processed} vaults processed`) - console.timeEnd('refreshReports') + console.timeEnd('refresh vault_reports') } if (require.main === module) { diff --git a/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts b/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts index 8b410573..4f6c60ea 100644 --- a/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/snapshot/[chainId]/[address]/route.ts @@ -1,7 +1,9 @@ import { NextRequest, NextResponse } from 'next/server' -import { getSnapshotKey } from '../../redis' -import { keyv } from '../../../cache' +import { createKeyvClient } from '../../../cache' import type { VaultSnapshot } from '../../db' +import { getSnapshotKey } from '../../redis' + +const keyv = createKeyvClient() export const runtime = 'nodejs' diff --git a/packages/web/app/api/rest/snapshot/refresh-snapshot.ts b/packages/web/app/api/rest/snapshot/refresh-snapshot.ts index b654f49e..1b66b226 100644 --- a/packages/web/app/api/rest/snapshot/refresh-snapshot.ts +++ b/packages/web/app/api/rest/snapshot/refresh-snapshot.ts @@ -1,6 +1,8 @@ +import { createKeyvClient } from '../cache' import { getVaults, getVaultSnapshot } from './db' import { getSnapshotKey } from './redis' -import { keyv } from '../cache' + +const keyv = createKeyvClient() const BATCH_SIZE = 10 @@ -26,7 +28,7 @@ async function refresh(): Promise { const entries = snapshots.filter((s): s is NonNullable => s !== null) if (entries.length > 0) { - await (keyv as any).setMany(entries) + await keyv.setMany(entries) } processed += entries.length diff --git a/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts b/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts index 44fad190..01bf20ca 100644 --- a/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server' +import { createKeyvClient } from '../../../../cache' import { labels } from '../../../labels' import { getTimeseriesKey } from '../../../redis' -import { keyv } from '../../../../cache' export const runtime = 'nodejs' @@ -11,14 +11,13 @@ type RouteParams = { address?: string | string[] } -type TimeseriesEntry = { time: number; component: string; value: number } -type ConsolidatedTimeseries = Record - const corsHeaders = { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,OPTIONS', } +const timeseriesKeyv = createKeyvClient() + export async function GET( request: NextRequest, context: { params: Promise }, @@ -46,16 +45,18 @@ export async function GET( : [entry.defaultComponent] const addressLower = address.toLowerCase() - const cacheKey = getTimeseriesKey(Number(chainId), addressLower) - let consolidated: ConsolidatedTimeseries | undefined + const cacheKey = getTimeseriesKey(entry.label, Number(chainId), addressLower) + let cached try { - consolidated = await keyv.get(cacheKey) + cached = await timeseriesKeyv.get(cacheKey) } catch (err) { console.error(`Redis read failed for ${cacheKey}:`, err) throw err } + const parsed: Array<{ time: number; component: string; value: number }> = cached + ? JSON.parse(cached as string) + : [] - const parsed = consolidated?.[entry.label] ?? [] const filtered = parsed.filter((row) => components.includes(row.component)) return NextResponse.json(filtered, { diff --git a/packages/web/app/api/rest/timeseries/redis.ts b/packages/web/app/api/rest/timeseries/redis.ts index 2ef3303c..ca97fc35 100644 --- a/packages/web/app/api/rest/timeseries/redis.ts +++ b/packages/web/app/api/rest/timeseries/redis.ts @@ -1,6 +1,7 @@ export function getTimeseriesKey( + label: string, chainId: number, addressLower: string, ): string { - return `timeseries:${chainId}:${addressLower.toLowerCase()}` + return `timeseries:${label}:${chainId}:${addressLower.toLowerCase()}` } diff --git a/packages/web/app/api/rest/timeseries/refresh-timeseries.ts b/packages/web/app/api/rest/timeseries/refresh-timeseries.ts index df8eccd4..110e9ed5 100644 --- a/packages/web/app/api/rest/timeseries/refresh-timeseries.ts +++ b/packages/web/app/api/rest/timeseries/refresh-timeseries.ts @@ -1,7 +1,9 @@ -import { labels } from './labels' +import { createKeyvClient } from '../cache' import { getFullTimeseries, getVaults, TimeseriesRow } from './db' +import { labels } from './labels' import { getTimeseriesKey } from './redis' -import { keyv } from '../cache' + +const keyv = createKeyvClient() const BATCH_SIZE = 10 @@ -16,10 +18,10 @@ async function refresh24hr(): Promise { for (let i = 0; i < vaults.length; i += BATCH_SIZE) { const batch = vaults.slice(i, i + BATCH_SIZE) - const results = await Promise.all(batch.map(async (vault) => { - const addressLower = vault.address.toLowerCase() + const entries: Array<{ key: string; value: unknown }> = [] - const labelData: Record> = {} + await Promise.all(batch.map(async (vault) => { + const addressLower = vault.address.toLowerCase() await Promise.all(labels.map(async ({ label }) => { const rows: TimeseriesRow[] = await getFullTimeseries( @@ -28,22 +30,22 @@ async function refresh24hr(): Promise { label, ) - labelData[label] = rows.map(row => ({ + const minimal = rows.map(row => ({ time: Number(row.time), component: row.component, value: row.value, })) - })) - return { - key: getTimeseriesKey(vault.chainId, addressLower), - value: labelData, - } + entries.push({ + key: getTimeseriesKey(label, vault.chainId, addressLower), + value: JSON.stringify(minimal), + }) + })) })) - await (keyv as any).setMany(results) + await keyv.setMany(entries) - processed += results.length + processed += batch.length if (processed % 10 === 0) { console.log(`Processed ${processed}/${vaults.length} vaults`) } diff --git a/packages/web/package.json b/packages/web/package.json index 0f06e9da..b91598c9 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -12,9 +12,9 @@ }, "dependencies": { "@apollo/server": "^4.9.5", - "@apollo/utils.keyvadapter": "^3.1.0", + "@apollo/utils.keyvadapter": "4.0.1", "@as-integrations/next": "^3.0.0", - "@keyv/redis": "^4.4.1", + "@keyv/redis": "5.1.6", "autoprefixer": "10.4.15", "bullmq": "^5.21.2", "chart.js": "^4.4.0", From 7e35bbb46f08319cd09e4269247540289e910f00 Mon Sep 17 00:00:00 2001 From: matheus1lva Date: Wed, 18 Feb 2026 16:33:45 -0300 Subject: [PATCH 3/5] fix --- packages/web/app/api/rest/list/vaults/route.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/app/api/rest/list/vaults/route.ts b/packages/web/app/api/rest/list/vaults/route.ts index 291ef432..e5ca3e1e 100644 --- a/packages/web/app/api/rest/list/vaults/route.ts +++ b/packages/web/app/api/rest/list/vaults/route.ts @@ -16,7 +16,7 @@ export async function GET(request: Request) { const origin = searchParams.get('origin') try { - const allVaults = await keyv.get(getListKey('all')) as VaultListItem[] | undefined + const allVaults = await keyv.get('all') as VaultListItem[] | undefined if (!allVaults) { return new NextResponse('Not found', { status: 404, headers: corsHeaders }) From 449195a962644840703cbf4e85e9a49680cd5a6f Mon Sep 17 00:00:00 2001 From: matheus1lva Date: Thu, 19 Feb 2026 15:44:49 -0300 Subject: [PATCH 4/5] double parse --- packages/web/app/api/rest/list/refresh.ts | 2 +- .../api/rest/timeseries/[segment]/[chainId]/[address]/route.ts | 2 +- packages/web/app/api/rest/timeseries/refresh-timeseries.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/web/app/api/rest/list/refresh.ts b/packages/web/app/api/rest/list/refresh.ts index 8d963e9c..834a3e0e 100644 --- a/packages/web/app/api/rest/list/refresh.ts +++ b/packages/web/app/api/rest/list/refresh.ts @@ -27,7 +27,7 @@ async function refresh(): Promise { await keyv.setMany(entries) console.log(`✓ Completed: ${vaults.length} vaults cached across ${chainIds.length} chains`) - console.timeEnd('refresh') + console.timeEnd('refresh list:vaults') } if (require.main === module) { diff --git a/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts b/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts index 01bf20ca..4f4cc2a9 100644 --- a/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts +++ b/packages/web/app/api/rest/timeseries/[segment]/[chainId]/[address]/route.ts @@ -54,7 +54,7 @@ export async function GET( throw err } const parsed: Array<{ time: number; component: string; value: number }> = cached - ? JSON.parse(cached as string) + ? (cached as Array<{ time: number; component: string; value: number }>) : [] const filtered = parsed.filter((row) => components.includes(row.component)) diff --git a/packages/web/app/api/rest/timeseries/refresh-timeseries.ts b/packages/web/app/api/rest/timeseries/refresh-timeseries.ts index 110e9ed5..a4facd49 100644 --- a/packages/web/app/api/rest/timeseries/refresh-timeseries.ts +++ b/packages/web/app/api/rest/timeseries/refresh-timeseries.ts @@ -38,7 +38,7 @@ async function refresh24hr(): Promise { entries.push({ key: getTimeseriesKey(label, vault.chainId, addressLower), - value: JSON.stringify(minimal), + value: minimal, }) })) })) From f1011cfdfed387de7b920301b66af35c3ea22dcb Mon Sep 17 00:00:00 2001 From: matheus1lva Date: Mon, 23 Feb 2026 13:55:06 -0300 Subject: [PATCH 5/5] update timeout --- packages/web/app/api/db/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/web/app/api/db/index.ts b/packages/web/app/api/db/index.ts index cac3cae9..c62d6d00 100644 --- a/packages/web/app/api/db/index.ts +++ b/packages/web/app/api/db/index.ts @@ -14,7 +14,7 @@ const db = new Pool({ password: process.env.POSTGRES_PASSWORD ?? 'password', max: parseInt(process.env.POSTGRES_POOL_MAX ?? '4', 10), idleTimeoutMillis: 60_000, - connectionTimeoutMillis: 50_000, + connectionTimeoutMillis: 5_000, }) export default db