feat: implement distributed caching

This commit is contained in:
mrjvs
2026-08-19 13:48:47 +02:00
parent 2390b6c6fd
commit 5a31197dcf
8 changed files with 134 additions and 32 deletions

79
server/utils/cache.ts Normal file
View File

@@ -0,0 +1,79 @@
import Redis from 'ioredis';
import type { H3Event } from 'h3';
let cacher: Cacher | null = null;
export type Cacher = {
set<T>(key: string, val: T, cacheAgeMs?: number): Promise<void>;
get<T>(key: string): Promise<T | null>;
};
function createCacher(event: H3Event): Cacher {
const config = useRuntimeConfig(event);
if (config.redisUrl) {
const redis = new Redis(config.redisUrl);
const prefix = `pn:website:cache`;
return {
async get(key) {
try {
const result = await redis.get(`${prefix}:${key}`);
if (result) {
return JSON.parse(result);
}
} catch {
// Catch connection & serialisation errors
}
return null;
},
async set(key, val, cacheAgeMs) {
try {
const fullKey = `${prefix}:${key}`;
await redis.set(fullKey, JSON.stringify(val));
if (cacheAgeMs) {
await redis.expire(fullKey, Math.round(cacheAgeMs / 1000));
}
} catch {
// Catch connection & serialisation errors
}
}
};
}
// Memory cacher
const store = new Map<string, { expiresAt: Date | null; value: string }>();
return {
async get(key) {
try {
const result = store.get(key);
if (result) {
if (result.expiresAt && result.expiresAt < new Date()) {
store.delete(key);
return null; // Expired
}
return JSON.parse(result.value);
}
} catch {
// catch Serialisation errors
}
return null;
},
async set(key, val, cacheAgeMs) {
try {
store.set(key, {
expiresAt: cacheAgeMs ? new Date(Date.now() + cacheAgeMs) : null,
value: JSON.stringify(val)
});
} catch {
// Catch serialisation errors
}
}
};
}
export function useCacher(event: H3Event): Cacher | null {
if (!cacher) {
cacher = createCacher(event);
}
return cacher;
}

View File

@@ -16,8 +16,6 @@ export type GithubProjectResponse = {
};
const orgName = 'PretendoNetwork';
const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour
let cache: { response: GithubProjectResponse; createdAt: Date } | null = null;
const getProjectsWithItemsV2GQL = `
query getProjectsV2($orgName: String!, $cursor: String) {
@@ -128,19 +126,24 @@ async function getGithubProjectsData(octokit: Octokit): Promise<GithubProjectRes
};
}
export async function getGithubProjects(octokit: Octokit | null, ignoreCache = false): Promise<GithubProjectResponse> {
if (!cache || new Date(cache.createdAt.getTime() + cacheMaxAgeMs) < new Date() || ignoreCache) {
// No github credentials, assume there are no projects
if (!octokit) {
return {
projects: []
};
}
const cacheKey = 'githubProjects';
const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour
cache = {
createdAt: new Date(),
response: await getGithubProjectsData(octokit)
export async function getGithubProjects(cacher: Cacher, octokit: Octokit | null, ignoreCache = false): Promise<GithubProjectResponse> {
if (!octokit) {
return {
projects: []
};
}
return cache.response;
if (!ignoreCache) {
const cached = await cacher.get<GithubProjectResponse>(cacheKey);
if (cached) {
return cached;
}
}
const data = await getGithubProjectsData(octokit);
await cacher.set(cacheKey, data, cacheMaxAgeMs);
return data;
}

View File

@@ -5,9 +5,6 @@ export type StripeDonationResponse = {
totalDonationsCents: number;
};
const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour
let cache: { response: StripeDonationResponse; createdAt: Date } | null = null;
async function getStripeDonationData(stripe: Stripe): Promise<StripeDonationResponse> {
const donationData: StripeDonationResponse = {
donatorCount: 0,
@@ -29,21 +26,26 @@ async function getStripeDonationData(stripe: Stripe): Promise<StripeDonationResp
return donationData;
}
export async function getStripeDonations(stripe: Stripe | null, ignoreCache = false): Promise<StripeDonationResponse> {
if (!cache || new Date(cache.createdAt.getTime() + cacheMaxAgeMs) < new Date() || ignoreCache) {
// No credentials, fill in blank data
if (!stripe) {
return {
donatorCount: 0,
totalDonationsCents: 0
};
}
const cacheKey = 'stripeDonations';
const cacheMaxAgeMs = 60 * 60 * 1000; // 1 hour
cache = {
createdAt: new Date(),
response: await getStripeDonationData(stripe)
export async function getStripeDonations(cacher: Cacher, stripe: Stripe | null, ignoreCache = false): Promise<StripeDonationResponse> {
if (!stripe) {
// No credentials, fill in blank data
return {
donatorCount: 0,
totalDonationsCents: 0
};
}
return cache.response;
if (!ignoreCache) {
const cached = await cacher.get<StripeDonationResponse>(cacheKey);
if (cached) {
return cached;
}
}
const data = await getStripeDonationData(stripe);
await cacher.set(cacheKey, data, cacheMaxAgeMs);
return data;
}