From b3bc4ee406c60dd22beae33c050cf08c675c70ee Mon Sep 17 00:00:00 2001 From: Kieran Klukas Date: Tue, 27 Jan 2026 14:45:35 -0500 Subject: [PATCH] format: format it all --- src/cache.ts | 106 ++++++++----- src/index.ts | 166 ++++++++++----------- src/migrations/bucketAnalyticsMigration.ts | 4 +- src/routes/api-routes.ts | 17 ++- src/slackWrapper.ts | 6 +- 5 files changed, 176 insertions(+), 123 deletions(-) diff --git a/src/cache.ts b/src/cache.ts index df2db5f..84df14c 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -394,19 +394,19 @@ class Cache { private optimizeSQLite() { // Enable Write-Ahead Logging for better concurrency this.db.run("PRAGMA journal_mode = WAL"); - + // NORMAL synchronous mode is faster and still safe with WAL this.db.run("PRAGMA synchronous = NORMAL"); - + // Increase cache size to 64MB for better query performance this.db.run("PRAGMA cache_size = -64000"); - + // Store temporary tables in memory this.db.run("PRAGMA temp_store = MEMORY"); - + // Enable memory-mapped I/O for faster reads (256MB) this.db.run("PRAGMA mmap_size = 268435456"); - + console.log("SQLite performance optimizations applied"); } @@ -458,7 +458,9 @@ class Cache { // Cache lookup statements this.stmtGetUser = this.db.prepare("SELECT * FROM users WHERE userId = ?"); - this.stmtGetEmoji = this.db.prepare("SELECT * FROM emojis WHERE name = ? AND expiration > ?"); + this.stmtGetEmoji = this.db.prepare( + "SELECT * FROM emojis WHERE name = ? AND expiration > ?", + ); } /** @@ -599,7 +601,9 @@ class Cache { // Find and close any orphaned sessions (from crashes) const orphanedSessions = this.db - .query("SELECT id, start_time FROM uptime_sessions WHERE end_time IS NULL") + .query( + "SELECT id, start_time FROM uptime_sessions WHERE end_time IS NULL", + ) .all() as Array<{ id: number; start_time: number }>; for (const session of orphanedSessions) { @@ -608,16 +612,20 @@ class Cache { .query("SELECT MAX(bucket) * 1000 as last_bucket FROM traffic_10min") .get() as { last_bucket: number | null }; - const estimatedEnd = lastActivity?.last_bucket && lastActivity.last_bucket > session.start_time - ? lastActivity.last_bucket - : session.start_time + 60000; // Assume at least 1 minute if no activity + const estimatedEnd = + lastActivity?.last_bucket && + lastActivity.last_bucket > session.start_time + ? lastActivity.last_bucket + : session.start_time + 60000; // Assume at least 1 minute if no activity const duration = estimatedEnd - session.start_time; this.db.run( "UPDATE uptime_sessions SET end_time = ?, duration = ? WHERE id = ?", [estimatedEnd, duration, session.id], ); - console.log(`Closed orphaned session ${session.id} (likely crash), estimated duration: ${Math.round(duration / 1000)}s`); + console.log( + `Closed orphaned session ${session.id} (likely crash), estimated duration: ${Math.round(duration / 1000)}s`, + ); } // Start new session @@ -666,7 +674,9 @@ class Cache { // Sum all completed session durations const completedResult = this.db - .query("SELECT COALESCE(SUM(duration), 0) as total FROM uptime_sessions WHERE duration IS NOT NULL") + .query( + "SELECT COALESCE(SUM(duration), 0) as total FROM uptime_sessions WHERE duration IS NOT NULL", + ) .get() as { total: number }; // Add current session duration (still running) @@ -674,7 +684,9 @@ class Cache { .query("SELECT start_time FROM uptime_sessions WHERE id = ?") .get(this.currentSessionId) as { start_time: number } | null; - const currentDuration = currentSession ? now - currentSession.start_time : 0; + const currentDuration = currentSession + ? now - currentSession.start_time + : 0; const totalUptime = completedResult.total + currentDuration; return Math.min(100, (totalUptime / totalLifetime) * 100); @@ -1214,7 +1226,10 @@ class Cache { * @returns Emoji object if found and not expired, null otherwise */ async getEmoji(name: string): Promise { - const result = this.stmtGetEmoji.get(name.toLowerCase(), Date.now()) as Emoji; + const result = this.stmtGetEmoji.get( + name.toLowerCase(), + Date.now(), + ) as Emoji; return result ? { @@ -1334,15 +1349,30 @@ class Cache { return "API Documentation"; } else if (endpoint === "/emojis") { return "Emoji List"; - } else if (endpoint.match(/^\/emojis\/[^/]+$/) || endpoint === "/emojis/EMOJI_NAME") { + } else if ( + endpoint.match(/^\/emojis\/[^/]+$/) || + endpoint === "/emojis/EMOJI_NAME" + ) { return "Emoji Data"; - } else if (endpoint.match(/^\/emojis\/[^/]+\/r$/) || endpoint === "/emojis/EMOJI_NAME/r") { + } else if ( + endpoint.match(/^\/emojis\/[^/]+\/r$/) || + endpoint === "/emojis/EMOJI_NAME/r" + ) { return "Emoji Redirects"; - } else if (endpoint.match(/^\/users\/[^/]+$/) || endpoint === "/users/USER_ID") { + } else if ( + endpoint.match(/^\/users\/[^/]+$/) || + endpoint === "/users/USER_ID" + ) { return "User Data"; - } else if (endpoint.match(/^\/users\/[^/]+\/r$/) || endpoint === "/users/USER_ID/r") { + } else if ( + endpoint.match(/^\/users\/[^/]+\/r$/) || + endpoint === "/users/USER_ID/r" + ) { return "User Redirects"; - } else if (endpoint.match(/^\/users\/[^/]+\/purge$/) || endpoint === "/reset") { + } else if ( + endpoint.match(/^\/users\/[^/]+\/purge$/) || + endpoint === "/reset" + ) { return "Cache Management"; } else if (endpoint.includes("/users/") && endpoint.includes("/r")) { return "User Redirects"; @@ -1570,7 +1600,10 @@ class Cache { WHERE bucket >= ? AND endpoint != '/stats' `, ) - .get(alignedCutoff) as { totalTime: number | null; totalHits: number | null }; + .get(alignedCutoff) as { + totalTime: number | null; + totalHits: number | null; + }; const averageResponseTime = avgResponseResult.totalHits && avgResponseResult.totalHits > 0 @@ -1599,7 +1632,11 @@ class Cache { p99: null as number | null, }; - const distribution: Array<{ range: string; count: number; percentage: number }> = []; + const distribution: Array<{ + range: string; + count: number; + percentage: number; + }> = []; // Slowest endpoints from grouped data const slowestEndpoints = requestsByEndpoint @@ -1811,7 +1848,10 @@ class Cache { .query( `SELECT SUM(total_response_time) as totalTime, SUM(hits) as totalHits FROM ${table} WHERE bucket >= ? AND endpoint != '/stats' AND total_response_time > 0`, ) - .get(alignedCutoff) as { totalTime: number | null; totalHits: number | null }; + .get(alignedCutoff) as { + totalTime: number | null; + totalHits: number | null; + }; // Error rate from bucket table (query kept for potential future use) this.db @@ -1915,11 +1955,9 @@ class Cache { * @param options - Either days for relative range, or start/end for absolute range * @returns Array of bucket data points with hits and latency */ - getTraffic(options: { - days?: number; - startTime?: number; - endTime?: number; - } = {}): Array<{ bucket: number; hits: number; avgLatency: number | null }> { + getTraffic( + options: { days?: number; startTime?: number; endTime?: number } = {}, + ): Array<{ bucket: number; hits: number; avgLatency: number | null }> { const now = Math.floor(Date.now() / 1000); let start: number; let end: number; @@ -1951,14 +1989,14 @@ class Cache { ORDER BY bucket ASC `, ) - .all(alignedStart, end) as Array<{ - bucket: number; - hits: number; - totalTime: number; - hitsWithTime: number; - }>; + .all(alignedStart, end) as Array<{ + bucket: number; + hits: number; + totalTime: number; + hitsWithTime: number; + }>; - return results.map(r => ({ + return results.map((r) => ({ bucket: r.bucket, hits: r.hits, avgLatency: r.hitsWithTime > 0 ? r.totalTime / r.hitsWithTime : null, diff --git a/src/index.ts b/src/index.ts index a5b9487..faef75a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,59 +10,59 @@ import swagger from "./swagger.html"; // Initialize Sentry if DSN is provided if (process.env.SENTRY_DSN) { - console.log("Sentry DSN provided, error monitoring is enabled"); - Sentry.init({ - environment: process.env.NODE_ENV, - dsn: process.env.SENTRY_DSN, - tracesSampleRate: 0.5, - ignoreErrors: ["Not Found", "404", "user_not_found", "emoji_not_found"], - }); + console.log("Sentry DSN provided, error monitoring is enabled"); + Sentry.init({ + environment: process.env.NODE_ENV, + dsn: process.env.SENTRY_DSN, + tracesSampleRate: 0.5, + ignoreErrors: ["Not Found", "404", "user_not_found", "emoji_not_found"], + }); } else { - console.warn("Sentry DSN not provided, error monitoring is disabled"); + console.warn("Sentry DSN not provided, error monitoring is disabled"); } // Initialize SlackWrapper and Cache const slackApp = new SlackWrapper(); const cache = new SlackCache( - process.env.DATABASE_PATH ?? "./data/cachet.db", - 25, - async () => { - console.log("Fetching emojis from Slack"); - const emojis = await slackApp.getEmojiList(); - const emojiEntries = Object.entries(emojis) - .map(([name, url]) => { - if (typeof url === "string" && url.startsWith("alias:")) { - const aliasName = url.substring(6); - const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName); - - if (!aliasUrl) { - console.warn(`Could not find alias for ${aliasName}`); - return null; - } - - return { - name, - imageUrl: aliasUrl, - alias: aliasName, - }; - } - return { - name, - imageUrl: url, - alias: null, - }; - }) - .filter( - ( - entry, - ): entry is { name: string; imageUrl: string; alias: string | null } => - entry !== null, - ); - - console.log("Batch inserting emojis"); - await cache.batchInsertEmojis(emojiEntries); - console.log("Finished batch inserting emojis"); - }, + process.env.DATABASE_PATH ?? "./data/cachet.db", + 25, + async () => { + console.log("Fetching emojis from Slack"); + const emojis = await slackApp.getEmojiList(); + const emojiEntries = Object.entries(emojis) + .map(([name, url]) => { + if (typeof url === "string" && url.startsWith("alias:")) { + const aliasName = url.substring(6); + const aliasUrl = emojis[aliasName] ?? getEmojiUrl(aliasName); + + if (!aliasUrl) { + console.warn(`Could not find alias for ${aliasName}`); + return null; + } + + return { + name, + imageUrl: aliasUrl, + alias: aliasName, + }; + } + return { + name, + imageUrl: url, + alias: null, + }; + }) + .filter( + ( + entry, + ): entry is { name: string; imageUrl: string; alias: string | null } => + entry !== null, + ); + + console.log("Batch inserting emojis"); + await cache.batchInsertEmojis(emojiEntries); + console.log("Finished batch inserting emojis"); + }, ); // Inject SlackWrapper into cache for background user updates @@ -77,56 +77,56 @@ const generatedSwagger = getSwaggerSpec(); // Legacy routes (non-API) const legacyRoutes = { - "/dashboard": dashboard, - "/swagger": swagger, - "/swagger.json": async (_: Request) => { - return Response.json(generatedSwagger); - }, - "/favicon.ico": async (_: Request) => { - return new Response(Bun.file("./favicon.ico")); - }, - - // Root route - redirect to dashboard for browsers - "/": async (request: Request) => { - const userAgent = request.headers.get("user-agent") || ""; - - if ( - userAgent.toLowerCase().includes("mozilla") || - userAgent.toLowerCase().includes("chrome") || - userAgent.toLowerCase().includes("safari") - ) { - return new Response(null, { - status: 302, - headers: { Location: "/dashboard" }, - }); - } - - return new Response( - "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---", - ); - }, + "/dashboard": dashboard, + "/swagger": swagger, + "/swagger.json": async (_: Request) => { + return Response.json(generatedSwagger); + }, + "/favicon.ico": async (_: Request) => { + return new Response(Bun.file("./favicon.ico")); + }, + + // Root route - redirect to dashboard for browsers + "/": async (request: Request) => { + const userAgent = request.headers.get("user-agent") || ""; + + if ( + userAgent.toLowerCase().includes("mozilla") || + userAgent.toLowerCase().includes("chrome") || + userAgent.toLowerCase().includes("safari") + ) { + return new Response(null, { + status: 302, + headers: { Location: "/dashboard" }, + }); + } + + return new Response( + "Hello World from Cachet 😊\n\n---\nSee /swagger for docs\nSee /dashboard for analytics\n---", + ); + }, }; // Merge all routes const allRoutes = { - ...legacyRoutes, - ...typedRoutes, + ...legacyRoutes, + ...typedRoutes, }; // Start the server const server = serve({ - routes: allRoutes, - port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, - development: process.env.NODE_ENV === "dev", + routes: allRoutes, + port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, + development: process.env.NODE_ENV === "dev", }); console.log(`🚀 Server running on http://localhost:${server.port}`); // Graceful shutdown handling const shutdown = () => { - console.log("Shutting down gracefully..."); - cache.endUptimeSession(); - process.exit(0); + console.log("Shutting down gracefully..."); + cache.endUptimeSession(); + process.exit(0); }; process.on("SIGINT", shutdown); diff --git a/src/migrations/bucketAnalyticsMigration.ts b/src/migrations/bucketAnalyticsMigration.ts index a49d8a2..a116ebb 100644 --- a/src/migrations/bucketAnalyticsMigration.ts +++ b/src/migrations/bucketAnalyticsMigration.ts @@ -188,7 +188,9 @@ export const bucketAnalyticsMigration: Migration = { db.run("DROP TABLE IF EXISTS request_analytics"); // Note: VACUUM cannot run inside a transaction, run manually after migration if needed - console.log("Bucket analytics migration completed (run VACUUM manually to reclaim space)"); + console.log( + "Bucket analytics migration completed (run VACUUM manually to reclaim space)", + ); }, async down(db: Database): Promise { diff --git a/src/routes/api-routes.ts b/src/routes/api-routes.ts index 8ce38a5..1d8d883 100644 --- a/src/routes/api-routes.ts +++ b/src/routes/api-routes.ts @@ -535,14 +535,24 @@ export function createApiRoutes(cache: SlackCache, slackApp: SlackWrapper) { "/stats/essential": { GET: createRoute( - withAnalytics("/stats/essential", "GET", handlers.handleGetEssentialStats), + withAnalytics( + "/stats/essential", + "GET", + handlers.handleGetEssentialStats, + ), { summary: "Get essential stats", description: "Fast-loading essential statistics for the dashboard", tags: ["Analytics"], parameters: { query: [ - queryParam("days", "number", "Number of days to analyze", false, 7), + queryParam( + "days", + "number", + "Number of days to analyze", + false, + 7, + ), ], }, responses: Object.fromEntries([ @@ -587,7 +597,8 @@ export function createApiRoutes(cache: SlackCache, slackApp: SlackWrapper) { withAnalytics("/stats/referers", "GET", handlers.handleGetReferers), { summary: "Get referer sources", - description: "Cumulative referer host statistics showing traffic sources", + description: + "Cumulative referer host statistics showing traffic sources", tags: ["Analytics"], responses: Object.fromEntries([ apiResponse(200, "Referers retrieved", { diff --git a/src/slackWrapper.ts b/src/slackWrapper.ts index 862acdb..134692c 100644 --- a/src/slackWrapper.ts +++ b/src/slackWrapper.ts @@ -43,13 +43,15 @@ class SlackWrapper { const maxConcurrent = Number(process.env.SLACK_MAX_CONCURRENT ?? 3); const minTime = Number(process.env.SLACK_MIN_TIME_MS ?? 200); // ~5 requests per second this.limiter = new Bottleneck({ - maxConcurrent: Number.isFinite(maxConcurrent) && maxConcurrent > 0 ? maxConcurrent : 3, + maxConcurrent: + Number.isFinite(maxConcurrent) && maxConcurrent > 0 ? maxConcurrent : 3, minTime: Number.isFinite(minTime) && minTime > 0 ? minTime : 200, }); // Request timeout in ms (default 5 seconds) const timeout = Number(process.env.SLACK_REQUEST_TIMEOUT_MS ?? 5000); - this.requestTimeout = Number.isFinite(timeout) && timeout > 0 ? timeout : 5000; + this.requestTimeout = + Number.isFinite(timeout) && timeout > 0 ? timeout : 5000; const missingFields = []; if (!this.signingSecret) missingFields.push("signing secret"); -- 2.51.2