express-performance-toolkit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +217 -0
  2. package/dist/cache.d.ts +25 -0
  3. package/dist/cache.d.ts.map +1 -0
  4. package/dist/cache.js +182 -0
  5. package/dist/cache.js.map +1 -0
  6. package/dist/compression.d.ts +7 -0
  7. package/dist/compression.d.ts.map +1 -0
  8. package/dist/compression.js +26 -0
  9. package/dist/compression.js.map +1 -0
  10. package/dist/dashboard/dashboard.html +756 -0
  11. package/dist/dashboard/dashboardRouter.d.ts +9 -0
  12. package/dist/dashboard/dashboardRouter.d.ts.map +1 -0
  13. package/dist/dashboard/dashboardRouter.js +71 -0
  14. package/dist/dashboard/dashboardRouter.js.map +1 -0
  15. package/dist/index.d.ts +45 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +130 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/logger.d.ts +8 -0
  20. package/dist/logger.d.ts.map +1 -0
  21. package/dist/logger.js +70 -0
  22. package/dist/logger.js.map +1 -0
  23. package/dist/queryHelper.d.ts +8 -0
  24. package/dist/queryHelper.d.ts.map +1 -0
  25. package/dist/queryHelper.js +39 -0
  26. package/dist/queryHelper.js.map +1 -0
  27. package/dist/store.d.ts +24 -0
  28. package/dist/store.d.ts.map +1 -0
  29. package/dist/store.js +108 -0
  30. package/dist/store.js.map +1 -0
  31. package/dist/types.d.ts +135 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +3 -0
  34. package/dist/types.js.map +1 -0
  35. package/example/server.ts +126 -0
  36. package/example/tsconfig.json +17 -0
  37. package/jest.config.js +10 -0
  38. package/package.json +57 -0
  39. package/src/cache.ts +228 -0
  40. package/src/compression.ts +25 -0
  41. package/src/dashboard/dashboard.html +756 -0
  42. package/src/dashboard/dashboardRouter.ts +45 -0
  43. package/src/index.ts +141 -0
  44. package/src/logger.ts +83 -0
  45. package/src/queryHelper.ts +49 -0
  46. package/src/store.ts +134 -0
  47. package/src/types.ts +155 -0
  48. package/tests/cache.test.ts +76 -0
  49. package/tests/integration.test.ts +124 -0
  50. package/tests/store.test.ts +103 -0
  51. package/tsconfig.json +21 -0
@@ -0,0 +1,45 @@
1
+ import { Router, Request, Response } from 'express';
2
+ import * as path from 'path';
3
+ import * as fs from 'fs';
4
+ import { MetricsStore } from '../store';
5
+ import { DashboardOptions } from '../types';
6
+
7
+ /**
8
+ * Create the dashboard Express router.
9
+ * Serves the HTML dashboard and JSON metrics API.
10
+ */
11
+ export function createDashboardRouter(
12
+ store: MetricsStore,
13
+ _options: DashboardOptions = {}
14
+ ): Router {
15
+ const router = Router();
16
+
17
+ // Cache the HTML file in memory
18
+ const htmlPath = path.join(__dirname, 'dashboard.html');
19
+ let dashboardHtml: string;
20
+
21
+ try {
22
+ dashboardHtml = fs.readFileSync(htmlPath, 'utf-8');
23
+ } catch {
24
+ dashboardHtml = '<h1>Dashboard HTML not found</h1><p>Ensure dashboard.html is in the dist/dashboard/ directory.</p>';
25
+ }
26
+
27
+ // Serve dashboard HTML
28
+ router.get('/', (_req: Request, res: Response) => {
29
+ res.set('Content-Type', 'text/html');
30
+ res.send(dashboardHtml);
31
+ });
32
+
33
+ // JSON metrics API endpoint
34
+ router.get('/api/metrics', (_req: Request, res: Response) => {
35
+ res.json(store.getMetrics());
36
+ });
37
+
38
+ // Reset metrics endpoint
39
+ router.post('/api/reset', (_req: Request, res: Response) => {
40
+ store.reset();
41
+ res.json({ success: true, message: 'Metrics reset' });
42
+ });
43
+
44
+ return router;
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { Request, Response, NextFunction, Router } from 'express';
2
+ import { MetricsStore } from './store';
3
+ import { createCacheMiddleware, LRUCache } from './cache';
4
+ import { createCompressionMiddleware } from './compression';
5
+ import { createLoggerMiddleware } from './logger';
6
+ import { createQueryHelperMiddleware } from './queryHelper';
7
+ import { createDashboardRouter } from './dashboard/dashboardRouter';
8
+ import {
9
+ ToolkitOptions,
10
+ CacheOptions,
11
+ CompressionOptions,
12
+ LoggerOptions,
13
+ QueryHelperOptions,
14
+ DashboardOptions,
15
+ Metrics,
16
+ CacheMiddleware,
17
+ } from './types';
18
+
19
+ export interface ToolkitInstance {
20
+ /** The composed Express middleware */
21
+ middleware: (req: Request, res: Response, next: NextFunction) => void;
22
+ /** The dashboard Express router — mount this if you want the dashboard */
23
+ dashboardRouter: Router;
24
+ /** Get current metrics snapshot */
25
+ getMetrics: () => Metrics;
26
+ /** Reset all metrics */
27
+ resetMetrics: () => void;
28
+ /** Access the cache middleware (for manual cache control) */
29
+ cache: CacheMiddleware | null;
30
+ /** The underlying metrics store */
31
+ store: MetricsStore;
32
+ }
33
+
34
+ /**
35
+ * ⚡ Express Performance Toolkit
36
+ *
37
+ * Creates a composable middleware stack that optimizes your Express app.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { performanceToolkit } from 'express-performance-toolkit';
42
+ *
43
+ * const toolkit = performanceToolkit({
44
+ * cache: true,
45
+ * logSlowRequests: true,
46
+ * dashboard: true,
47
+ * });
48
+ *
49
+ * app.use(toolkit.middleware);
50
+ * app.use('/__perf', toolkit.dashboardRouter);
51
+ * ```
52
+ */
53
+ export function performanceToolkit(options: ToolkitOptions = {}): ToolkitInstance {
54
+ const store = new MetricsStore({ maxLogs: options.maxLogs || 1000 });
55
+
56
+ const middlewares: ((req: Request, res: Response, next: NextFunction) => void)[] = [];
57
+ let cacheMiddlewareInstance: CacheMiddleware | null = null;
58
+
59
+ // ── Compression ──────────────────────────────────────────
60
+ const compressionConfig = normalizeOption<CompressionOptions>(options.compression, { enabled: true });
61
+ if (compressionConfig.enabled !== false) {
62
+ middlewares.push(createCompressionMiddleware(compressionConfig));
63
+ }
64
+
65
+ // ── Query Helper ─────────────────────────────────────────
66
+ const queryConfig = normalizeOption<QueryHelperOptions>(options.queryHelper, { enabled: false });
67
+ if (queryConfig.enabled !== false) {
68
+ middlewares.push(createQueryHelperMiddleware(queryConfig));
69
+ }
70
+
71
+ // ── Logger (slow request detection) ──────────────────────
72
+ const loggerConfig = normalizeOption<LoggerOptions>(options.logSlowRequests, { enabled: true });
73
+ if (loggerConfig.enabled !== false) {
74
+ middlewares.push(createLoggerMiddleware(loggerConfig, store));
75
+ }
76
+
77
+ // ── Cache ────────────────────────────────────────────────
78
+ const cacheConfig = normalizeOption<CacheOptions>(options.cache, { enabled: false });
79
+ if (cacheConfig.enabled !== false) {
80
+ cacheMiddlewareInstance = createCacheMiddleware(cacheConfig, store);
81
+ middlewares.push(cacheMiddlewareInstance);
82
+ }
83
+
84
+ // ── Dashboard Router ─────────────────────────────────────
85
+ const dashboardConfig = normalizeOption<DashboardOptions>(options.dashboard, { enabled: true });
86
+ const dashboardPath = dashboardConfig.path || '/__perf';
87
+ const dashboardRouter = createDashboardRouter(store, dashboardConfig);
88
+
89
+ // ── Composed Middleware ──────────────────────────────────
90
+ function composedMiddleware(req: Request, res: Response, next: NextFunction): void {
91
+ let index = 0;
92
+
93
+ function runNext(err?: unknown): void {
94
+ if (err) return next(err);
95
+ if (index >= middlewares.length) return next();
96
+
97
+ const current = middlewares[index++];
98
+ try {
99
+ current(req, res, runNext);
100
+ } catch (e) {
101
+ next(e);
102
+ }
103
+ }
104
+
105
+ runNext();
106
+ }
107
+
108
+ return {
109
+ middleware: composedMiddleware,
110
+ dashboardRouter,
111
+ getMetrics: () => store.getMetrics(),
112
+ resetMetrics: () => store.reset(),
113
+ cache: cacheMiddlewareInstance,
114
+ store,
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Normalize a boolean | object option into a config object.
120
+ * - `true` → defaults with enabled: true
121
+ * - `false` → defaults with enabled: false
122
+ * - object → merged with defaults
123
+ */
124
+ function normalizeOption<T extends { enabled?: boolean }>(
125
+ value: boolean | T | undefined,
126
+ defaults: T
127
+ ): T {
128
+ if (value === true) return { ...defaults, enabled: true };
129
+ if (value === false) return { ...defaults, enabled: false };
130
+ if (typeof value === 'object') return { ...defaults, ...value, enabled: true };
131
+ return defaults;
132
+ }
133
+
134
+ // ── Re-exports ─────────────────────────────────────────────
135
+ export { MetricsStore } from './store';
136
+ export { LRUCache, createCacheMiddleware } from './cache';
137
+ export { createCompressionMiddleware } from './compression';
138
+ export { createLoggerMiddleware } from './logger';
139
+ export { createQueryHelperMiddleware } from './queryHelper';
140
+ export { createDashboardRouter } from './dashboard/dashboardRouter';
141
+ export * from './types';
package/src/logger.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import onFinished from 'on-finished';
3
+ import { LoggerOptions, LogEntry } from './types';
4
+ import { MetricsStore } from './store';
5
+
6
+ /**
7
+ * Default log formatter for console output.
8
+ */
9
+ function defaultFormatter(entry: LogEntry): string {
10
+ const slow = entry.slow ? ' 🔥 SLOW' : '';
11
+ const cached = entry.cached ? ' [CACHED]' : '';
12
+ const status = entry.statusCode;
13
+ const time = `${entry.responseTime}ms`;
14
+
15
+ return `[perf] ${entry.method} ${entry.path} → ${status} ${time}${cached}${slow}`;
16
+ }
17
+
18
+ /**
19
+ * Create request logging & slow API detection middleware.
20
+ */
21
+ export function createLoggerMiddleware(
22
+ options: LoggerOptions = {},
23
+ store: MetricsStore
24
+ ): (req: Request, res: Response, next: NextFunction) => void {
25
+ const {
26
+ slowThreshold = 1000,
27
+ console: logToConsole = true,
28
+ formatter = defaultFormatter,
29
+ } = options;
30
+
31
+ return (req: Request, res: Response, next: NextFunction): void => {
32
+ const startTime = Date.now();
33
+
34
+ // Attach perf data to request
35
+ if (!req.perfToolkit) {
36
+ req.perfToolkit = {
37
+ startTime,
38
+ queryCount: 0,
39
+ trackQuery: (label?: string) => {
40
+ req.perfToolkit!.queryCount++;
41
+ },
42
+ };
43
+ }
44
+
45
+ onFinished(res, (_err, finishedRes) => {
46
+ const responseTime = Date.now() - startTime;
47
+ const isSlow = responseTime >= slowThreshold;
48
+
49
+ const entry: LogEntry = {
50
+ method: req.method,
51
+ path: req.originalUrl || req.url,
52
+ statusCode: finishedRes.statusCode,
53
+ responseTime,
54
+ timestamp: Date.now(),
55
+ slow: isSlow,
56
+ cached: res.getHeader('X-Cache') === 'HIT',
57
+ queryCount: req.perfToolkit?.queryCount,
58
+ contentLength: parseInt(res.getHeader('content-length') as string, 10) || undefined,
59
+ userAgent: req.get('user-agent'),
60
+ ip: req.ip,
61
+ };
62
+
63
+ // Record in store
64
+ store.addLog(entry);
65
+
66
+ if (isSlow) {
67
+ store.recordSlowRequest();
68
+ }
69
+
70
+ // Console output
71
+ if (logToConsole) {
72
+ const message = formatter(entry);
73
+ if (isSlow) {
74
+ console.warn(message);
75
+ } else {
76
+ console.log(message);
77
+ }
78
+ }
79
+ });
80
+
81
+ next();
82
+ };
83
+ }
@@ -0,0 +1,49 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import { QueryHelperOptions } from './types';
3
+
4
+ /**
5
+ * Create query optimization helper middleware.
6
+ * Tracks database queries per request and warns about potential N+1 issues.
7
+ */
8
+ export function createQueryHelperMiddleware(
9
+ options: QueryHelperOptions = {}
10
+ ): (req: Request, res: Response, next: NextFunction) => void {
11
+ const { threshold = 10 } = options;
12
+
13
+ return (req: Request, _res: Response, next: NextFunction): void => {
14
+ const queries: { label: string; timestamp: number }[] = [];
15
+
16
+ // Initialize or extend perfToolkit on the request
17
+ if (!req.perfToolkit) {
18
+ req.perfToolkit = {
19
+ startTime: Date.now(),
20
+ queryCount: 0,
21
+ trackQuery: () => {},
22
+ };
23
+ }
24
+
25
+ req.perfToolkit.trackQuery = (label?: string): void => {
26
+ req.perfToolkit!.queryCount++;
27
+ queries.push({
28
+ label: label || `query-${req.perfToolkit!.queryCount}`,
29
+ timestamp: Date.now(),
30
+ });
31
+
32
+ // Warn if threshold exceeded
33
+ if (req.perfToolkit!.queryCount === threshold) {
34
+ console.warn(
35
+ `[perf] ⚠️ N+1 Alert: ${req.method} ${req.originalUrl || req.url} ` +
36
+ `has made ${threshold}+ queries. Consider optimizing with batch/join queries.`
37
+ );
38
+ console.warn(
39
+ `[perf] Recent queries: ${queries
40
+ .slice(-5)
41
+ .map((q) => q.label)
42
+ .join(', ')}`
43
+ );
44
+ }
45
+ };
46
+
47
+ next();
48
+ };
49
+ }
package/src/store.ts ADDED
@@ -0,0 +1,134 @@
1
+ import { LogEntry, Metrics, RouteStats } from './types';
2
+
3
+ /**
4
+ * In-memory metrics store — shared state between all middleware components.
5
+ * Uses a ring buffer for request logs and counters for aggregate stats.
6
+ */
7
+ export class MetricsStore {
8
+ private maxLogs: number;
9
+ private logs: LogEntry[];
10
+ private stats: {
11
+ totalRequests: number;
12
+ totalResponseTime: number;
13
+ slowRequests: number;
14
+ cacheHits: number;
15
+ cacheMisses: number;
16
+ cacheSize: number;
17
+ statusCodes: Record<number, number>;
18
+ routes: Record<string, RouteStats>;
19
+ startTime: number;
20
+ };
21
+
22
+ constructor(options: { maxLogs?: number } = {}) {
23
+ this.maxLogs = options.maxLogs || 1000;
24
+ this.logs = [];
25
+ this.stats = {
26
+ totalRequests: 0,
27
+ totalResponseTime: 0,
28
+ slowRequests: 0,
29
+ cacheHits: 0,
30
+ cacheMisses: 0,
31
+ cacheSize: 0,
32
+ statusCodes: {},
33
+ routes: {},
34
+ startTime: Date.now(),
35
+ };
36
+ }
37
+
38
+ /** Add a request log entry to the ring buffer. */
39
+ addLog(entry: LogEntry): void {
40
+ this.logs.push({
41
+ ...entry,
42
+ timestamp: entry.timestamp || Date.now(),
43
+ });
44
+
45
+ // Ring buffer — drop oldest entries when full
46
+ if (this.logs.length > this.maxLogs) {
47
+ this.logs.shift();
48
+ }
49
+
50
+ // Update aggregate stats
51
+ this.stats.totalRequests++;
52
+ this.stats.totalResponseTime += entry.responseTime || 0;
53
+
54
+ // Track status codes
55
+ const code = entry.statusCode || 0;
56
+ this.stats.statusCodes[code] = (this.stats.statusCodes[code] || 0) + 1;
57
+
58
+ // Track per-route stats
59
+ const routeKey = `${entry.method} ${entry.path}`;
60
+ if (!this.stats.routes[routeKey]) {
61
+ this.stats.routes[routeKey] = {
62
+ count: 0,
63
+ totalTime: 0,
64
+ slowCount: 0,
65
+ avgTime: 0,
66
+ };
67
+ }
68
+ const route = this.stats.routes[routeKey];
69
+ route.count++;
70
+ route.totalTime += entry.responseTime || 0;
71
+ route.avgTime = Math.round(route.totalTime / route.count);
72
+ if (entry.slow) {
73
+ route.slowCount++;
74
+ }
75
+ }
76
+
77
+ recordSlowRequest(): void {
78
+ this.stats.slowRequests++;
79
+ }
80
+
81
+ recordCacheHit(): void {
82
+ this.stats.cacheHits++;
83
+ }
84
+
85
+ recordCacheMiss(): void {
86
+ this.stats.cacheMisses++;
87
+ }
88
+
89
+ setCacheSize(size: number): void {
90
+ this.stats.cacheSize = size;
91
+ }
92
+
93
+ /** Get all metrics data for the dashboard. */
94
+ getMetrics(): Metrics {
95
+ const avgResponseTime =
96
+ this.stats.totalRequests > 0
97
+ ? Math.round(this.stats.totalResponseTime / this.stats.totalRequests)
98
+ : 0;
99
+
100
+ const cacheTotal = this.stats.cacheHits + this.stats.cacheMisses;
101
+ const cacheHitRate =
102
+ cacheTotal > 0
103
+ ? Math.round((this.stats.cacheHits / cacheTotal) * 100)
104
+ : 0;
105
+
106
+ return {
107
+ uptime: Date.now() - this.stats.startTime,
108
+ totalRequests: this.stats.totalRequests,
109
+ avgResponseTime,
110
+ slowRequests: this.stats.slowRequests,
111
+ cacheHits: this.stats.cacheHits,
112
+ cacheMisses: this.stats.cacheMisses,
113
+ cacheHitRate,
114
+ cacheSize: this.stats.cacheSize,
115
+ statusCodes: { ...this.stats.statusCodes },
116
+ routes: { ...this.stats.routes },
117
+ recentLogs: this.logs.slice(-100),
118
+ };
119
+ }
120
+
121
+ /** Reset all metrics. */
122
+ reset(): void {
123
+ this.logs = [];
124
+ this.stats.totalRequests = 0;
125
+ this.stats.totalResponseTime = 0;
126
+ this.stats.slowRequests = 0;
127
+ this.stats.cacheHits = 0;
128
+ this.stats.cacheMisses = 0;
129
+ this.stats.cacheSize = 0;
130
+ this.stats.statusCodes = {};
131
+ this.stats.routes = {};
132
+ this.stats.startTime = Date.now();
133
+ }
134
+ }
package/src/types.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+
3
+ // ─── Configuration Types ────────────────────────────────────────────
4
+
5
+ export interface CacheOptions {
6
+ /** Enable caching (default: true) */
7
+ enabled?: boolean;
8
+ /** Cache TTL in milliseconds (default: 60000) */
9
+ ttl?: number;
10
+ /** Max entries in LRU cache (default: 100) */
11
+ maxSize?: number;
12
+ /** URL patterns to exclude from caching */
13
+ exclude?: (string | RegExp)[];
14
+ /** HTTP methods to cache (default: ['GET']) */
15
+ methods?: string[];
16
+ /** Redis config — requires ioredis as peer dep */
17
+ redis?: RedisConfig | null;
18
+ }
19
+
20
+ export interface RedisConfig {
21
+ host?: string;
22
+ port?: number;
23
+ password?: string;
24
+ db?: number;
25
+ prefix?: string;
26
+ ttl?: number;
27
+ [key: string]: unknown;
28
+ }
29
+
30
+ export interface CompressionOptions {
31
+ /** Enable compression (default: true) */
32
+ enabled?: boolean;
33
+ /** Minimum response size to compress in bytes (default: 1024) */
34
+ threshold?: number;
35
+ /** Compression level 1-9 (default: 6) */
36
+ level?: number;
37
+ }
38
+
39
+ export interface LoggerOptions {
40
+ /** Enable request logging (default: true) */
41
+ enabled?: boolean;
42
+ /** Slow request threshold in ms (default: 1000) */
43
+ slowThreshold?: number;
44
+ /** Log to console (default: true) */
45
+ console?: boolean;
46
+ /** Custom log formatter */
47
+ formatter?: (entry: LogEntry) => string;
48
+ }
49
+
50
+ export interface QueryHelperOptions {
51
+ /** Enable query tracking (default: false) */
52
+ enabled?: boolean;
53
+ /** Warn if more than this many queries per request (default: 10) */
54
+ threshold?: number;
55
+ }
56
+
57
+ export interface DashboardOptions {
58
+ /** Enable dashboard (default: true) */
59
+ enabled?: boolean;
60
+ /** Dashboard mount path (default: '/__perf') */
61
+ path?: string;
62
+ }
63
+
64
+ export interface ToolkitOptions {
65
+ /** Cache configuration — pass true for defaults or an object to customize */
66
+ cache?: boolean | CacheOptions;
67
+ /** Compression configuration — pass true for defaults or an object */
68
+ compression?: boolean | CompressionOptions;
69
+ /** Slow request detection — pass true for defaults or an object */
70
+ logSlowRequests?: boolean | LoggerOptions;
71
+ /** Query optimization helper */
72
+ queryHelper?: boolean | QueryHelperOptions;
73
+ /** Performance dashboard */
74
+ dashboard?: boolean | DashboardOptions;
75
+ /** Max log entries to keep in memory (default: 1000) */
76
+ maxLogs?: number;
77
+ }
78
+
79
+ // ─── Data Types ─────────────────────────────────────────────────────
80
+
81
+ export interface LogEntry {
82
+ method: string;
83
+ path: string;
84
+ statusCode: number;
85
+ responseTime: number;
86
+ timestamp: number;
87
+ slow: boolean;
88
+ cached: boolean;
89
+ queryCount?: number;
90
+ contentLength?: number;
91
+ userAgent?: string;
92
+ ip?: string;
93
+ }
94
+
95
+ export interface RouteStats {
96
+ count: number;
97
+ totalTime: number;
98
+ slowCount: number;
99
+ avgTime: number;
100
+ }
101
+
102
+ export interface Metrics {
103
+ uptime: number;
104
+ totalRequests: number;
105
+ avgResponseTime: number;
106
+ slowRequests: number;
107
+ cacheHits: number;
108
+ cacheMisses: number;
109
+ cacheHitRate: number;
110
+ cacheSize: number;
111
+ statusCodes: Record<number, number>;
112
+ routes: Record<string, RouteStats>;
113
+ recentLogs: LogEntry[];
114
+ }
115
+
116
+ export interface CacheEntry {
117
+ body: string | Buffer;
118
+ statusCode: number;
119
+ contentType: string | undefined;
120
+ }
121
+
122
+ export interface LRUCacheEntry<T> {
123
+ value: T;
124
+ createdAt: number;
125
+ }
126
+
127
+ export interface CacheAdapter<T = CacheEntry> {
128
+ get(key: string): T | null | Promise<T | null>;
129
+ set(key: string, value: T): void | Promise<void>;
130
+ has(key: string): boolean | Promise<boolean>;
131
+ delete(key: string): boolean | void | Promise<boolean | void>;
132
+ clear(): void | Promise<void>;
133
+ readonly size: number;
134
+ }
135
+
136
+ export interface CacheMiddleware {
137
+ (req: Request, res: Response, next: NextFunction): void;
138
+ clear: () => void | Promise<void>;
139
+ delete: (key: string) => boolean | void | Promise<boolean | void>;
140
+ adapter: CacheAdapter;
141
+ }
142
+
143
+ // ─── Express Augmentation ───────────────────────────────────────────
144
+
145
+ declare global {
146
+ namespace Express {
147
+ interface Request {
148
+ perfToolkit?: {
149
+ startTime: number;
150
+ queryCount: number;
151
+ trackQuery: (label?: string) => void;
152
+ };
153
+ }
154
+ }
155
+ }
@@ -0,0 +1,76 @@
1
+ import { LRUCache } from '../src/cache';
2
+
3
+ describe('LRUCache', () => {
4
+ let cache: LRUCache<string>;
5
+
6
+ beforeEach(() => {
7
+ cache = new LRUCache<string>({ maxSize: 3, ttl: 5000 });
8
+ });
9
+
10
+ it('should store and retrieve values', () => {
11
+ cache.set('key1', 'value1');
12
+ expect(cache.get('key1')).toBe('value1');
13
+ });
14
+
15
+ it('should return null for missing keys', () => {
16
+ expect(cache.get('nonexistent')).toBeNull();
17
+ });
18
+
19
+ it('should evict oldest entry when max size is reached', () => {
20
+ cache.set('a', '1');
21
+ cache.set('b', '2');
22
+ cache.set('c', '3');
23
+ cache.set('d', '4'); // should evict 'a'
24
+
25
+ expect(cache.get('a')).toBeNull();
26
+ expect(cache.get('b')).toBe('2');
27
+ expect(cache.get('d')).toBe('4');
28
+ expect(cache.size).toBe(3);
29
+ });
30
+
31
+ it('should expire entries after TTL', () => {
32
+ const shortCache = new LRUCache<string>({ maxSize: 10, ttl: 50 });
33
+ shortCache.set('key', 'value');
34
+ expect(shortCache.get('key')).toBe('value');
35
+
36
+ // Fast-forward by manipulating the internal state
37
+ const entry = (shortCache as any).cache.get('key');
38
+ entry.createdAt = Date.now() - 100; // expired
39
+ expect(shortCache.get('key')).toBeNull();
40
+ });
41
+
42
+ it('should report correct size', () => {
43
+ expect(cache.size).toBe(0);
44
+ cache.set('a', '1');
45
+ expect(cache.size).toBe(1);
46
+ cache.set('b', '2');
47
+ expect(cache.size).toBe(2);
48
+ });
49
+
50
+ it('should delete entries', () => {
51
+ cache.set('key', 'value');
52
+ expect(cache.delete('key')).toBe(true);
53
+ expect(cache.get('key')).toBeNull();
54
+ });
55
+
56
+ it('should clear all entries', () => {
57
+ cache.set('a', '1');
58
+ cache.set('b', '2');
59
+ cache.clear();
60
+ expect(cache.size).toBe(0);
61
+ });
62
+
63
+ it('should update recently used position on get', () => {
64
+ cache.set('a', '1');
65
+ cache.set('b', '2');
66
+ cache.set('c', '3');
67
+
68
+ // Access 'a' to make it recently used
69
+ cache.get('a');
70
+
71
+ // Adding 'd' should evict 'b' (now oldest), not 'a'
72
+ cache.set('d', '4');
73
+ expect(cache.get('a')).toBe('1');
74
+ expect(cache.get('b')).toBeNull();
75
+ });
76
+ });