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.
- package/README.md +217 -0
- package/dist/cache.d.ts +25 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +182 -0
- package/dist/cache.js.map +1 -0
- package/dist/compression.d.ts +7 -0
- package/dist/compression.d.ts.map +1 -0
- package/dist/compression.js +26 -0
- package/dist/compression.js.map +1 -0
- package/dist/dashboard/dashboard.html +756 -0
- package/dist/dashboard/dashboardRouter.d.ts +9 -0
- package/dist/dashboard/dashboardRouter.d.ts.map +1 -0
- package/dist/dashboard/dashboardRouter.js +71 -0
- package/dist/dashboard/dashboardRouter.js.map +1 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +130 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +8 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +70 -0
- package/dist/logger.js.map +1 -0
- package/dist/queryHelper.d.ts +8 -0
- package/dist/queryHelper.d.ts.map +1 -0
- package/dist/queryHelper.js +39 -0
- package/dist/queryHelper.js.map +1 -0
- package/dist/store.d.ts +24 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +108 -0
- package/dist/store.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/example/server.ts +126 -0
- package/example/tsconfig.json +17 -0
- package/jest.config.js +10 -0
- package/package.json +57 -0
- package/src/cache.ts +228 -0
- package/src/compression.ts +25 -0
- package/src/dashboard/dashboard.html +756 -0
- package/src/dashboard/dashboardRouter.ts +45 -0
- package/src/index.ts +141 -0
- package/src/logger.ts +83 -0
- package/src/queryHelper.ts +49 -0
- package/src/store.ts +134 -0
- package/src/types.ts +155 -0
- package/tests/cache.test.ts +76 -0
- package/tests/integration.test.ts +124 -0
- package/tests/store.test.ts +103 -0
- 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
|
+
});
|