mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-08-23 09:07:40 -05:00
feat: implement distributed caching
This commit is contained in:
@@ -41,6 +41,7 @@ export default defineNuxtConfig({
|
||||
envPrefix: 'PN_WEBSITE_'
|
||||
},
|
||||
|
||||
trustProxy: false,
|
||||
githubApiToken: '',
|
||||
stripeSecretKey: '',
|
||||
stripeWebhookSecret: '',
|
||||
@@ -65,6 +66,7 @@ export default defineNuxtConfig({
|
||||
discourseSsoSecret: '',
|
||||
apiBase: 'https://api.pretendo.cc',
|
||||
apiBaseHost: 'api.pretendo.cc',
|
||||
redisUrl: '',
|
||||
|
||||
public: {
|
||||
baseUrl: 'https://pretendo.network',
|
||||
|
||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -27,6 +27,7 @@
|
||||
"eslint": "^9.39.5",
|
||||
"feed": "^6.0.0",
|
||||
"hcaptcha": "^0.2.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"mlly": "^1.8.2",
|
||||
"mongodb": "^7.5.0",
|
||||
"nice-grpc": "^2.1.17",
|
||||
@@ -35,6 +36,7 @@
|
||||
"nuxt": "^4.5.2",
|
||||
"octokit": "^5.0.5",
|
||||
"papr": "^17.1.1",
|
||||
"rate-limiter-flexible": "^11.2.0",
|
||||
"reka-ui": "^2.10.3",
|
||||
"stripe": "^22.4.0",
|
||||
"text-mask-core": "^5.1.2",
|
||||
@@ -19167,6 +19169,12 @@
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/rate-limiter-flexible": {
|
||||
"version": "11.2.0",
|
||||
"resolved": "https://registry.npmjs.org/rate-limiter-flexible/-/rate-limiter-flexible-11.2.0.tgz",
|
||||
"integrity": "sha512-L0eIK+BmFMi6NcvmtEg7RSswOFKi9MMD5RhBIypFfOve+G6Jl1Xbb8qEHgK3uRzNtkOFYF0L9f7P4rSf2PnUVw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"eslint": "^9.39.5",
|
||||
"feed": "^6.0.0",
|
||||
"hcaptcha": "^0.2.0",
|
||||
"ioredis": "^5.11.1",
|
||||
"mlly": "^1.8.2",
|
||||
"mongodb": "^7.5.0",
|
||||
"nice-grpc": "^2.1.17",
|
||||
@@ -40,6 +41,7 @@
|
||||
"nuxt": "^4.5.2",
|
||||
"octokit": "^5.0.5",
|
||||
"papr": "^17.1.1",
|
||||
"rate-limiter-flexible": "^11.2.0",
|
||||
"reka-ui": "^2.10.3",
|
||||
"stripe": "^22.4.0",
|
||||
"text-mask-core": "^5.1.2",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getGithubProjects } from '../utils/getGithubProgress';
|
||||
import { getStripeDonations } from '../utils/getStripeDonations';
|
||||
import { useCacher } from '../utils/cache';
|
||||
import type { GetProgress, ProgressItem } from '#shared/api-types';
|
||||
|
||||
const donationGoalCents = 3000 * 100;
|
||||
@@ -7,8 +8,9 @@ const donationGoalCents = 3000 * 100;
|
||||
export default defineEventHandler(async (event): Promise<GetProgress> => {
|
||||
const octokit = useOctokit(event);
|
||||
const stripe = useStripe(event);
|
||||
const donationData = await getStripeDonations(stripe);
|
||||
const { projects } = await getGithubProjects(octokit);
|
||||
const cacher = useCacher(event);
|
||||
const donationData = await getStripeDonations(cacher, stripe);
|
||||
const { projects } = await getGithubProjects(cacher, octokit);
|
||||
|
||||
const items: ProgressItem[] = projects.map((v) => {
|
||||
const totalTasks = v.tasks.length;
|
||||
|
||||
79
server/utils/cache.ts
Normal file
79
server/utils/cache.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
4
src/plugins/types.d.ts
vendored
4
src/plugins/types.d.ts
vendored
@@ -6,6 +6,8 @@ declare module '#app' {
|
||||
|
||||
declare module 'nuxt/schema' {
|
||||
interface RuntimeConfig {
|
||||
trustProxy: boolean;
|
||||
|
||||
githubApiToken: string;
|
||||
|
||||
stripeSecretKey: string;
|
||||
@@ -38,6 +40,8 @@ declare module 'nuxt/schema' {
|
||||
|
||||
apiBase: string;
|
||||
apiBaseHost: string;
|
||||
|
||||
redisUrl: string;
|
||||
}
|
||||
|
||||
interface PublicRuntimeConfig {
|
||||
|
||||
Reference in New Issue
Block a user