From b542d825539a650de9473b2df8b42241732d6407 Mon Sep 17 00:00:00 2001 From: Owen Date: Fri, 24 Oct 2025 11:14:07 -0700 Subject: [PATCH] Consolidate into central cache --- server/lib/billing/usageService.ts | 12 +++--------- server/lib/cache.ts | 5 +++++ server/private/lib/certificates.ts | 11 +++++------ server/routers/badger/verifySession.ts | 18 ++++++------------ server/routers/newt/dockerSocket.ts | 5 ----- server/routers/newt/handleSocketMessages.ts | 8 ++++---- server/routers/site/listSites.ts | 8 +++----- server/routers/site/socketIntegration.ts | 12 ++++++------ server/routers/user/inviteUser.ts | 8 +++----- 9 files changed, 35 insertions(+), 52 deletions(-) create mode 100644 server/lib/cache.ts diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts index 999d5b63..8e6f5e9c 100644 --- a/server/lib/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -1,5 +1,4 @@ import { eq, sql, and } from "drizzle-orm"; -import NodeCache from "node-cache"; import { v4 as uuidv4 } from "uuid"; import { PutObjectCommand } from "@aws-sdk/client-s3"; import * as fs from "fs/promises"; @@ -20,6 +19,7 @@ import logger from "@server/logger"; import { sendToClient } from "#dynamic/routers/ws"; import { build } from "@server/build"; import { s3Client } from "@server/lib/s3"; +import cache from "@server/lib/cache"; interface StripeEvent { identifier?: string; @@ -43,7 +43,6 @@ export function noop() { } export class UsageService { - private cache: NodeCache; private bucketName: string | undefined; private currentEventFile: string | null = null; private currentFileStartTime: number = 0; @@ -51,7 +50,6 @@ export class UsageService { private uploadingFiles: Set = new Set(); constructor() { - this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL if (noop()) { return; } @@ -399,7 +397,7 @@ export class UsageService { featureId: FeatureId ): Promise { const cacheKey = `customer_${orgId}_${featureId}`; - const cached = this.cache.get(cacheKey); + const cached = cache.get(cacheKey); if (cached) { return cached; @@ -422,7 +420,7 @@ export class UsageService { const customerId = customer.customerId; // Cache the result - this.cache.set(cacheKey, customerId); + cache.set(cacheKey, customerId, 300); // 5 minute TTL return customerId; } catch (error) { @@ -700,10 +698,6 @@ export class UsageService { await this.uploadFileToS3(); } - public clearCache(): void { - this.cache.flushAll(); - } - /** * Scan the events directory for files older than 1 minute and upload them if not empty. */ diff --git a/server/lib/cache.ts b/server/lib/cache.ts new file mode 100644 index 00000000..efa7d201 --- /dev/null +++ b/server/lib/cache.ts @@ -0,0 +1,5 @@ +import NodeCache from "node-cache"; + +export const cache = new NodeCache({ stdTTL: 3600, checkperiod: 120 }); + +export default cache; \ No newline at end of file diff --git a/server/private/lib/certificates.ts b/server/private/lib/certificates.ts index 2ca967be..ec4b73ee 100644 --- a/server/private/lib/certificates.ts +++ b/server/private/lib/certificates.ts @@ -16,8 +16,8 @@ import { certificates, db } from "@server/db"; import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm"; import { decryptData } from "@server/lib/encryption"; import * as fs from "fs"; -import NodeCache from "node-cache"; import logger from "@server/logger"; +import cache from "@server/lib/cache"; let encryptionKeyPath = ""; let encryptionKeyHex = ""; @@ -51,9 +51,6 @@ export type CertificateResult = { updatedAt?: number | null; }; -// --- In-Memory Cache Implementation --- -const certificateCache = new NodeCache({ stdTTL: 180 }); // Cache for 3 minutes (180 seconds) - export async function getValidCertificatesForDomains( domains: Set, useCache: boolean = true @@ -67,7 +64,8 @@ export async function getValidCertificatesForDomains( // 1. Check cache first if enabled if (useCache) { for (const domain of domains) { - const cachedCert = certificateCache.get(domain); + const cacheKey = `cert:${domain}`; + const cachedCert = cache.get(cacheKey); if (cachedCert) { finalResults.push(cachedCert); // Valid cache hit } else { @@ -180,7 +178,8 @@ export async function getValidCertificatesForDomains( // Add to cache for future requests, using the *requested domain* as the key if (useCache) { - certificateCache.set(domain, resultCert); + const cacheKey = `cert:${domain}`; + cache.set(cacheKey, resultCert, 180); } } } diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 523163e6..8a33f1ea 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -17,7 +17,6 @@ import { import { LoginPage, Resource, - ResourceAccessToken, ResourceHeaderAuth, ResourcePassword, ResourcePincode, @@ -30,18 +29,13 @@ import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import NodeCache from "node-cache"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { getCountryCodeForIp } from "@server/lib/geoip"; import { getOrgTierData } from "#dynamic/lib/billing"; import { TierId } from "@server/lib/billing/tiers"; import { verifyPassword } from "@server/auth/password"; - -// We'll see if this speeds anything up -const cache = new NodeCache({ - stdTTL: 5 // seconds -}); +import cache from "@server/lib/cache"; const verifyResourceSessionSchema = z.object({ sessions: z.record(z.string()).optional(), @@ -153,7 +147,7 @@ export async function verifyResourceSession( } resourceData = result; - cache.set(resourceCacheKey, resourceData); + cache.set(resourceCacheKey, resourceData, 5); } const { resource, pincode, password, headerAuth } = resourceData; @@ -308,7 +302,7 @@ export async function verifyResourceSession( headerAuth.headerAuthHash ) ) { - cache.set(clientHeaderAuthKey, clientHeaderAuth); + cache.set(clientHeaderAuthKey, clientHeaderAuth, 5); logger.debug("Resource allowed because header auth is valid"); return allowed(res); } @@ -360,7 +354,7 @@ export async function verifyResourceSession( ); resourceSession = result?.resourceSession; - cache.set(sessionCacheKey, resourceSession); + cache.set(sessionCacheKey, resourceSession, 5); } if (resourceSession?.isRequestToken) { @@ -423,7 +417,7 @@ export async function verifyResourceSession( resource ); - cache.set(userAccessCacheKey, allowedUserData); + cache.set(userAccessCacheKey, allowedUserData, 5); } if ( @@ -629,7 +623,7 @@ async function checkRules( if (!rules) { rules = await getResourceRules(resourceId); - cache.set(ruleCacheKey, rules); + cache.set(ruleCacheKey, rules, 5); } if (rules.length === 0) { diff --git a/server/routers/newt/dockerSocket.ts b/server/routers/newt/dockerSocket.ts index 3847d9a4..41f36d3a 100644 --- a/server/routers/newt/dockerSocket.ts +++ b/server/routers/newt/dockerSocket.ts @@ -1,10 +1,5 @@ -import NodeCache from "node-cache"; import { sendToClient } from "#dynamic/routers/ws"; -export const dockerSocketCache = new NodeCache({ - stdTTL: 3600 // seconds -}); - export function fetchContainers(newtId: string) { const payload = { type: `newt/socket/fetch`, diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts index 0491393f..09a473b9 100644 --- a/server/routers/newt/handleSocketMessages.ts +++ b/server/routers/newt/handleSocketMessages.ts @@ -1,8 +1,8 @@ import { MessageHandler } from "@server/routers/ws"; import logger from "@server/logger"; -import { dockerSocketCache } from "./dockerSocket"; import { Newt } from "@server/db"; import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint"; +import cache from "@server/lib/cache"; export const handleDockerStatusMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; @@ -24,8 +24,8 @@ export const handleDockerStatusMessage: MessageHandler = async (context) => { if (available) { logger.info(`Newt ${newt.newtId} has Docker socket access`); - dockerSocketCache.set(`${newt.newtId}:socketPath`, socketPath, 0); - dockerSocketCache.set(`${newt.newtId}:isAvailable`, available, 0); + cache.set(`${newt.newtId}:socketPath`, socketPath, 0); + cache.set(`${newt.newtId}:isAvailable`, available, 0); } else { logger.warn(`Newt ${newt.newtId} does not have Docker socket access`); } @@ -54,7 +54,7 @@ export const handleDockerContainersMessage: MessageHandler = async ( ); if (containers && containers.length > 0) { - dockerSocketCache.set(`${newt.newtId}:dockerContainers`, containers, 0); + cache.set(`${newt.newtId}:dockerContainers`, containers, 0); } else { logger.warn(`Newt ${newt.newtId} does not have Docker containers`); } diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index 694556f7..cddf8c4b 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -9,14 +9,12 @@ import createHttpError from "http-errors"; import { z } from "zod"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; -import NodeCache from "node-cache"; import semver from "semver"; - -const newtVersionCache = new NodeCache({ stdTTL: 3600 }); // 1 hours in seconds +import cache from "@server/lib/cache"; async function getLatestNewtVersion(): Promise { try { - const cachedVersion = newtVersionCache.get("latestNewtVersion"); + const cachedVersion = cache.get("latestNewtVersion"); if (cachedVersion) { return cachedVersion; } @@ -48,7 +46,7 @@ async function getLatestNewtVersion(): Promise { const latestVersion = tags[0].name; - newtVersionCache.set("latestNewtVersion", latestVersion); + cache.set("latestNewtVersion", latestVersion); return latestVersion; } catch (error: any) { diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts index 7b5160cb..3a52dcd2 100644 --- a/server/routers/site/socketIntegration.ts +++ b/server/routers/site/socketIntegration.ts @@ -12,9 +12,9 @@ import stoi from "@server/lib/stoi"; import { sendToClient } from "#dynamic/routers/ws"; import { fetchContainers, - dockerSocketCache, dockerSocket } from "../newt/dockerSocket"; +import cache from "@server/lib/cache"; export interface ContainerNetwork { networkId: string; @@ -157,7 +157,7 @@ async function triggerFetch(siteId: number) { // clear the cache for this Newt ID so that the site has to keep asking for the containers // this is to ensure that the site always gets the latest data - dockerSocketCache.del(`${newt.newtId}:dockerContainers`); + cache.del(`${newt.newtId}:dockerContainers`); return { siteId, newtId: newt.newtId }; } @@ -165,7 +165,7 @@ async function triggerFetch(siteId: number) { async function queryContainers(siteId: number) { const { newt } = await getSiteAndNewt(siteId); - const result = dockerSocketCache.get( + const result = cache.get( `${newt.newtId}:dockerContainers` ) as Container[]; if (!result) { @@ -182,7 +182,7 @@ async function isDockerAvailable(siteId: number): Promise { const { newt } = await getSiteAndNewt(siteId); const key = `${newt.newtId}:isAvailable`; - const isAvailable = dockerSocketCache.get(key); + const isAvailable = cache.get(key); return !!isAvailable; } @@ -196,8 +196,8 @@ async function getDockerStatus( const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`); const result = { - isAvailable: dockerSocketCache.get(mappedKeys[0]) as boolean, - socketPath: dockerSocketCache.get(mappedKeys[1]) as string | undefined + isAvailable: cache.get(mappedKeys[0]) as boolean, + socketPath: cache.get(mappedKeys[1]) as string | undefined }; return result; diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 56098bea..f35fa785 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -1,4 +1,3 @@ -import NodeCache from "node-cache"; import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { db } from "@server/db"; @@ -20,8 +19,7 @@ import { UserType } from "@server/types/UserTypes"; import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; - -const regenerateTracker = new NodeCache({ stdTTL: 3600, checkperiod: 600 }); +import cache from "@server/lib/cache"; const inviteUserParamsSchema = z .object({ @@ -182,7 +180,7 @@ export async function inviteUser( } if (existingInvite.length) { - const attempts = regenerateTracker.get(email) || 0; + const attempts = cache.get(email) || 0; if (attempts >= 3) { return next( createHttpError( @@ -192,7 +190,7 @@ export async function inviteUser( ); } - regenerateTracker.set(email, attempts + 1); + cache.set(email, attempts + 1); const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId const token = generateRandomString(