Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/indexer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@
}
},
"dependencies": {
"@aave/math-utils": "1.29.1",
"@aave/contract-helpers": "1.29.1",
"@bull-board/api": "6.13.0",
"@bull-board/fastify": "6.13.0",
"@fastify/autoload": "6.0.3",
Expand Down
77 changes: 72 additions & 5 deletions apps/indexer/src/app/plugins/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,28 @@ import type {
import fp from 'fastify-plugin';
import { ENV } from '../../env';
import { encode } from '../../libs/encode';
import { logger } from '../../libs/logger';
import { createRedisConnection } from '../../libs/utils/redis';

// Custom JSON serialization to handle BigInt values
function serializeWithBigInt(value: unknown): string {
return JSON.stringify(value, (key, val) => {
if (typeof val === 'bigint') {
return { __type: 'bigint', value: val.toString() };
}
return val;
});
}

function deserializeWithBigInt<T>(json: string): T {
return JSON.parse(json, (key, val) => {
if (val && typeof val === 'object' && val.__type === 'bigint') {
return BigInt(val.value);
}
return val;
});
}

const cacheRedisConnection = createRedisConnection(
ENV.REDIS_URL,
ENV.REDIS_CLUSTER_MODE,
Expand Down Expand Up @@ -106,7 +126,7 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
req: FastifyRequest,
): RouteCacheOptions | null => {
const rawCfg = req.routeOptions.config.cache;
if (!rawCfg) return null;
if (!rawCfg || ENV.NO_CACHE) return null;

if (typeof rawCfg === 'boolean') {
if (!rawCfg) return null;
Expand Down Expand Up @@ -142,15 +162,15 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
(cfg.key?.(req) ??
encode.sha256(
`${routeUrl}:${req.raw.method}:` +
`${JSON.stringify(req.query ?? {})}:${JSON.stringify(req.body ?? {})}`,
`${JSON.stringify(req.params ?? {})}:${JSON.stringify(req.query ?? {})}:${JSON.stringify(req.body ?? {})}`,
));

req.__cacheKey = key;

const cached = await redis.get(key);
if (!cached) return;

const entry: CacheEntry = JSON.parse(cached);
const entry: CacheEntry = deserializeWithBigInt(cached);
const ageSec = (Date.now() - entry.storedAt) / 1000;

const isFresh = ageSec <= entry.ttlSeconds;
Expand Down Expand Up @@ -228,7 +248,7 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
const cfg: RouteCacheOptions =
typeof rawCfg === 'boolean' ? { enabled: rawCfg } : rawCfg;

if (cfg.enabled === false) return payload;
if (cfg.enabled === false || ENV.NO_CACHE) return payload;

if (req.__cacheHit) {
return payload;
Expand Down Expand Up @@ -260,7 +280,7 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (

const expireSeconds = ttl + (cfg.staleTtlSeconds ?? defaultStaleTtl);

await redis.setex(key, expireSeconds, JSON.stringify(entry));
await redis.setex(key, expireSeconds, serializeWithBigInt(entry));

// For "real" client requests (not internal revalidation), set MISS header
if (req.headers['x-cache-revalidate'] !== '1') {
Expand All @@ -272,6 +292,53 @@ const redisCachePlugin: FastifyPluginAsync<RedisCachePluginOptions> = async (
);
};

export const maybeCache = async <T = unknown>(
key: string,
fn: () => Promise<T>,
opts: Pick<RouteCacheOptions, 'ttlSeconds' | 'enabled'> = {},
): Promise<T> => {
const enabled = opts.enabled ?? true;

if (!enabled || ENV.NO_CACHE) {
return fn();
}

const redis = cacheRedisConnection;

const ttl = opts.ttlSeconds ?? 30; // 30 seconds

const cacheKey = 'maybe-cache:fn:' + encode.sha256(key);

const cached = await redis.get(cacheKey);

if (cached) {
const entry: CacheEntry = deserializeWithBigInt(cached);
const ageSec = (Date.now() - entry.storedAt) / 1000;

const isFresh = ageSec <= entry.ttlSeconds;

logger.info({ key, ageSec, ttl: entry.ttlSeconds }, 'Cache hit');

if (isFresh) {
return entry.payload as T;
}
}

logger.info({ key }, 'Cache miss, invoking function');

const entry = {
payload: await fn(),
headers: {},
statusCode: 200,
storedAt: Date.now(),
ttlSeconds: ttl,
};

await redis.setex(cacheKey, ttl, serializeWithBigInt(entry));

return entry.payload as T;
};

export default fp(redisCachePlugin, {
name: 'cache-plugin',
});
Loading