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,135 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ export interface CacheOptions {
3
+ /** Enable caching (default: true) */
4
+ enabled?: boolean;
5
+ /** Cache TTL in milliseconds (default: 60000) */
6
+ ttl?: number;
7
+ /** Max entries in LRU cache (default: 100) */
8
+ maxSize?: number;
9
+ /** URL patterns to exclude from caching */
10
+ exclude?: (string | RegExp)[];
11
+ /** HTTP methods to cache (default: ['GET']) */
12
+ methods?: string[];
13
+ /** Redis config — requires ioredis as peer dep */
14
+ redis?: RedisConfig | null;
15
+ }
16
+ export interface RedisConfig {
17
+ host?: string;
18
+ port?: number;
19
+ password?: string;
20
+ db?: number;
21
+ prefix?: string;
22
+ ttl?: number;
23
+ [key: string]: unknown;
24
+ }
25
+ export interface CompressionOptions {
26
+ /** Enable compression (default: true) */
27
+ enabled?: boolean;
28
+ /** Minimum response size to compress in bytes (default: 1024) */
29
+ threshold?: number;
30
+ /** Compression level 1-9 (default: 6) */
31
+ level?: number;
32
+ }
33
+ export interface LoggerOptions {
34
+ /** Enable request logging (default: true) */
35
+ enabled?: boolean;
36
+ /** Slow request threshold in ms (default: 1000) */
37
+ slowThreshold?: number;
38
+ /** Log to console (default: true) */
39
+ console?: boolean;
40
+ /** Custom log formatter */
41
+ formatter?: (entry: LogEntry) => string;
42
+ }
43
+ export interface QueryHelperOptions {
44
+ /** Enable query tracking (default: false) */
45
+ enabled?: boolean;
46
+ /** Warn if more than this many queries per request (default: 10) */
47
+ threshold?: number;
48
+ }
49
+ export interface DashboardOptions {
50
+ /** Enable dashboard (default: true) */
51
+ enabled?: boolean;
52
+ /** Dashboard mount path (default: '/__perf') */
53
+ path?: string;
54
+ }
55
+ export interface ToolkitOptions {
56
+ /** Cache configuration — pass true for defaults or an object to customize */
57
+ cache?: boolean | CacheOptions;
58
+ /** Compression configuration — pass true for defaults or an object */
59
+ compression?: boolean | CompressionOptions;
60
+ /** Slow request detection — pass true for defaults or an object */
61
+ logSlowRequests?: boolean | LoggerOptions;
62
+ /** Query optimization helper */
63
+ queryHelper?: boolean | QueryHelperOptions;
64
+ /** Performance dashboard */
65
+ dashboard?: boolean | DashboardOptions;
66
+ /** Max log entries to keep in memory (default: 1000) */
67
+ maxLogs?: number;
68
+ }
69
+ export interface LogEntry {
70
+ method: string;
71
+ path: string;
72
+ statusCode: number;
73
+ responseTime: number;
74
+ timestamp: number;
75
+ slow: boolean;
76
+ cached: boolean;
77
+ queryCount?: number;
78
+ contentLength?: number;
79
+ userAgent?: string;
80
+ ip?: string;
81
+ }
82
+ export interface RouteStats {
83
+ count: number;
84
+ totalTime: number;
85
+ slowCount: number;
86
+ avgTime: number;
87
+ }
88
+ export interface Metrics {
89
+ uptime: number;
90
+ totalRequests: number;
91
+ avgResponseTime: number;
92
+ slowRequests: number;
93
+ cacheHits: number;
94
+ cacheMisses: number;
95
+ cacheHitRate: number;
96
+ cacheSize: number;
97
+ statusCodes: Record<number, number>;
98
+ routes: Record<string, RouteStats>;
99
+ recentLogs: LogEntry[];
100
+ }
101
+ export interface CacheEntry {
102
+ body: string | Buffer;
103
+ statusCode: number;
104
+ contentType: string | undefined;
105
+ }
106
+ export interface LRUCacheEntry<T> {
107
+ value: T;
108
+ createdAt: number;
109
+ }
110
+ export interface CacheAdapter<T = CacheEntry> {
111
+ get(key: string): T | null | Promise<T | null>;
112
+ set(key: string, value: T): void | Promise<void>;
113
+ has(key: string): boolean | Promise<boolean>;
114
+ delete(key: string): boolean | void | Promise<boolean | void>;
115
+ clear(): void | Promise<void>;
116
+ readonly size: number;
117
+ }
118
+ export interface CacheMiddleware {
119
+ (req: Request, res: Response, next: NextFunction): void;
120
+ clear: () => void | Promise<void>;
121
+ delete: (key: string) => boolean | void | Promise<boolean | void>;
122
+ adapter: CacheAdapter;
123
+ }
124
+ declare global {
125
+ namespace Express {
126
+ interface Request {
127
+ perfToolkit?: {
128
+ startTime: number;
129
+ queryCount: number;
130
+ trackQuery: (label?: string) => void;
131
+ };
132
+ }
133
+ }
134
+ }
135
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,126 @@
1
+ import express, { Request, Response } from 'express';
2
+ import { performanceToolkit } from '../src/index';
3
+
4
+ const app = express();
5
+ const PORT = 3000;
6
+
7
+ // ── Initialize Performance Toolkit ──────────────────────────
8
+ const toolkit = performanceToolkit({
9
+ cache: {
10
+ ttl: 30000, // 30s cache TTL
11
+ maxSize: 50,
12
+ exclude: ['/api/random', '/__perf'],
13
+ },
14
+ compression: true,
15
+ logSlowRequests: {
16
+ slowThreshold: 500, // Flag requests > 500ms as slow
17
+ console: true,
18
+ },
19
+ queryHelper: {
20
+ threshold: 5,
21
+ },
22
+ dashboard: true,
23
+ });
24
+
25
+ // Apply the middleware
26
+ app.use(toolkit.middleware);
27
+
28
+ // Mount the dashboard
29
+ app.use('/__perf', toolkit.dashboardRouter);
30
+
31
+ // ── Sample API Routes ───────────────────────────────────────
32
+
33
+ // Fast route — should be cached after first hit
34
+ app.get('/api/users', (_req: Request, res: Response) => {
35
+ const users = [
36
+ { id: 1, name: 'Alice Johnson', email: 'alice@example.com', role: 'admin' },
37
+ { id: 2, name: 'Bob Smith', email: 'bob@example.com', role: 'user' },
38
+ { id: 3, name: 'Charlie Brown', email: 'charlie@example.com', role: 'user' },
39
+ { id: 4, name: 'Diana Prince', email: 'diana@example.com', role: 'moderator' },
40
+ { id: 5, name: 'Eve Wilson', email: 'eve@example.com', role: 'user' },
41
+ ];
42
+ res.json({ users, count: users.length });
43
+ });
44
+
45
+ // Slow route — simulates a 1.5s delay (triggers slow detection)
46
+ app.get('/api/slow', (_req: Request, res: Response) => {
47
+ setTimeout(() => {
48
+ res.json({
49
+ message: 'This response was intentionally delayed',
50
+ processingTime: '1500ms',
51
+ });
52
+ }, 1500);
53
+ });
54
+
55
+ // Medium-speed route
56
+ app.get('/api/products', (_req: Request, res: Response) => {
57
+ setTimeout(() => {
58
+ res.json({
59
+ products: [
60
+ { id: 1, name: 'Laptop Pro', price: 1299.99, stock: 25 },
61
+ { id: 2, name: 'Wireless Mouse', price: 29.99, stock: 150 },
62
+ { id: 3, name: 'USB-C Hub', price: 49.99, stock: 75 },
63
+ ],
64
+ });
65
+ }, 300);
66
+ });
67
+
68
+ // Random data — excluded from cache
69
+ app.get('/api/random', (_req: Request, res: Response) => {
70
+ res.json({
71
+ value: Math.random(),
72
+ timestamp: Date.now(),
73
+ });
74
+ });
75
+
76
+ // Route demonstrating query tracking
77
+ app.get('/api/posts', (req: Request, res: Response) => {
78
+ // Simulate multiple DB queries (potential N+1)
79
+ const posts = [];
80
+ for (let i = 0; i < 12; i++) {
81
+ req.perfToolkit?.trackQuery(`SELECT * FROM comments WHERE post_id=${i}`);
82
+ posts.push({
83
+ id: i,
84
+ title: `Post ${i}`,
85
+ commentCount: Math.floor(Math.random() * 20),
86
+ });
87
+ }
88
+ res.json({ posts });
89
+ });
90
+
91
+ // POST route — not cached
92
+ app.post('/api/users', express.json(), (req: Request, res: Response) => {
93
+ res.status(201).json({
94
+ message: 'User created',
95
+ user: req.body,
96
+ });
97
+ });
98
+
99
+ // Error route
100
+ app.get('/api/error', (_req: Request, _res: Response) => {
101
+ throw new Error('Something went wrong!');
102
+ });
103
+
104
+ // Error handler
105
+ app.use((err: Error, _req: Request, res: Response, _next: express.NextFunction) => {
106
+ console.error(err.stack);
107
+ res.status(500).json({ error: err.message });
108
+ });
109
+
110
+ // ── Start Server ────────────────────────────────────────────
111
+ app.listen(PORT, () => {
112
+ console.log('');
113
+ console.log(' ⚡ Express Performance Toolkit — Example Server');
114
+ console.log(' ────────────────────────────────────────────────');
115
+ console.log(` 🚀 Server: http://localhost:${PORT}`);
116
+ console.log(` 📊 Dashboard: http://localhost:${PORT}/__perf`);
117
+ console.log('');
118
+ console.log(' Try these endpoints:');
119
+ console.log(` GET http://localhost:${PORT}/api/users (fast, cached)`);
120
+ console.log(` GET http://localhost:${PORT}/api/slow (slow, triggers alert)`);
121
+ console.log(` GET http://localhost:${PORT}/api/products (medium speed)`);
122
+ console.log(` GET http://localhost:${PORT}/api/random (not cached)`);
123
+ console.log(` GET http://localhost:${PORT}/api/posts (N+1 query warning)`);
124
+ console.log(` POST http://localhost:${PORT}/api/users (not cached)`);
125
+ console.log('');
126
+ });
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "lib": ["ES2020"],
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "forceConsistentCasingInFileNames": true,
10
+ "resolveJsonModule": true,
11
+ "moduleResolution": "node",
12
+ "outDir": "./dist",
13
+ "rootDir": "."
14
+ },
15
+ "include": ["server.ts"],
16
+ "references": [{ "path": ".." }]
17
+ }
package/jest.config.js ADDED
@@ -0,0 +1,10 @@
1
+ /** @type {import('ts-jest').JestConfigWithTsJest} */
2
+ module.exports = {
3
+ preset: 'ts-jest',
4
+ testEnvironment: 'node',
5
+ roots: ['<rootDir>/tests'],
6
+ testMatch: ['**/*.test.ts'],
7
+ moduleFileExtensions: ['ts', 'js', 'json'],
8
+ collectCoverageFrom: ['src/**/*.ts', '!src/dashboard/dashboard.html'],
9
+ coverageDirectory: 'coverage',
10
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "express-performance-toolkit",
3
+ "version": "1.0.0",
4
+ "description": "⚡ A powerful Express middleware that automatically optimizes your app with request caching, response compression, slow API detection, and a real-time performance dashboard.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "postbuild": "npm run copy-assets",
10
+ "copy-assets": "mkdir -p dist/dashboard && cp src/dashboard/dashboard.html dist/dashboard/",
11
+ "dev": "tsc --watch",
12
+ "test": "jest --verbose",
13
+ "example": "ts-node example/server.ts",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "express",
18
+ "performance",
19
+ "middleware",
20
+ "caching",
21
+ "compression",
22
+ "monitoring",
23
+ "dashboard",
24
+ "slow-api",
25
+ "optimization",
26
+ "logging",
27
+ "typescript"
28
+ ],
29
+ "author": "",
30
+ "license": "MIT",
31
+ "dependencies": {
32
+ "compression": "^1.7.4",
33
+ "on-finished": "^2.4.1"
34
+ },
35
+ "peerDependencies": {
36
+ "express": ">=4.0.0"
37
+ },
38
+ "peerDependenciesMeta": {
39
+ "ioredis": {
40
+ "optional": true
41
+ }
42
+ },
43
+ "devDependencies": {
44
+ "@types/compression": "^1.7.5",
45
+ "@types/express": "^4.17.21",
46
+ "@types/jest": "^29.5.12",
47
+ "@types/node": "^20.11.0",
48
+ "@types/on-finished": "^2.3.4",
49
+ "@types/supertest": "^7.2.0",
50
+ "express": "^4.21.0",
51
+ "jest": "^29.7.0",
52
+ "supertest": "^6.3.3",
53
+ "ts-jest": "^29.1.2",
54
+ "ts-node": "^10.9.2",
55
+ "typescript": "^5.3.3"
56
+ }
57
+ }
package/src/cache.ts ADDED
@@ -0,0 +1,228 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ import {
3
+ CacheOptions,
4
+ CacheEntry,
5
+ CacheAdapter,
6
+ LRUCacheEntry,
7
+ CacheMiddleware,
8
+ } from "./types";
9
+ import { MetricsStore } from "./store";
10
+
11
+ /**
12
+ * In-memory LRU cache with TTL support.
13
+ */
14
+ export class LRUCache<T = CacheEntry> implements CacheAdapter<T> {
15
+ private maxSize: number;
16
+ private ttl: number;
17
+ private cache: Map<string, LRUCacheEntry<T>>;
18
+
19
+ constructor(options: { maxSize?: number; ttl?: number } = {}) {
20
+ this.maxSize = options.maxSize || 100;
21
+ this.ttl = options.ttl || 60000;
22
+ this.cache = new Map();
23
+ }
24
+
25
+ get(key: string): T | null {
26
+ const entry = this.cache.get(key);
27
+ if (!entry) return null;
28
+
29
+ // Check TTL
30
+ if (Date.now() - entry.createdAt > this.ttl) {
31
+ this.cache.delete(key);
32
+ return null;
33
+ }
34
+
35
+ // Move to end (most recently used)
36
+ this.cache.delete(key);
37
+ this.cache.set(key, entry);
38
+ return entry.value;
39
+ }
40
+
41
+ set(key: string, value: T): void {
42
+ // Remove oldest if at capacity
43
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
44
+ const firstKey = this.cache.keys().next().value;
45
+ if (firstKey !== undefined) {
46
+ this.cache.delete(firstKey);
47
+ }
48
+ }
49
+
50
+ this.cache.set(key, {
51
+ value,
52
+ createdAt: Date.now(),
53
+ });
54
+ }
55
+
56
+ has(key: string): boolean {
57
+ return this.get(key) !== null;
58
+ }
59
+
60
+ delete(key: string): boolean {
61
+ return this.cache.delete(key);
62
+ }
63
+
64
+ clear(): void {
65
+ this.cache.clear();
66
+ }
67
+
68
+ get size(): number {
69
+ return this.cache.size;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Create a Redis cache adapter (requires ioredis as peer dependency).
75
+ */
76
+ function createRedisAdapter(
77
+ redisConfig: Record<string, unknown>,
78
+ ): CacheAdapter | null {
79
+ try {
80
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
81
+ const Redis = require("ioredis");
82
+ const client = new Redis(redisConfig);
83
+ const prefix = (redisConfig.prefix as string) || "ept:";
84
+ const ttl = (redisConfig.ttl as number) || 60;
85
+
86
+ return {
87
+ async get(key: string): Promise<CacheEntry | null> {
88
+ const data = await client.get(`${prefix}${key}`);
89
+ return data ? JSON.parse(data) : null;
90
+ },
91
+ async set(key: string, value: CacheEntry): Promise<void> {
92
+ await client.setex(`${prefix}${key}`, ttl, JSON.stringify(value));
93
+ },
94
+ async has(key: string): Promise<boolean> {
95
+ return (await client.exists(`${prefix}${key}`)) === 1;
96
+ },
97
+ async delete(key: string): Promise<void> {
98
+ await client.del(`${prefix}${key}`);
99
+ },
100
+ async clear(): Promise<void> {
101
+ const keys = await client.keys(`${prefix}*`);
102
+ if (keys.length > 0) await client.del(...keys);
103
+ },
104
+ get size(): number {
105
+ return -1; // Cannot easily get size from Redis
106
+ },
107
+ };
108
+ } catch {
109
+ console.warn(
110
+ "[express-performance-toolkit] ioredis not installed. Falling back to in-memory cache.",
111
+ );
112
+ return null;
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Create cache middleware.
118
+ */
119
+ export function createCacheMiddleware(
120
+ options: CacheOptions = {},
121
+ store?: MetricsStore,
122
+ ): CacheMiddleware {
123
+ const {
124
+ ttl = 60000,
125
+ maxSize = 100,
126
+ exclude = [],
127
+ redis = null,
128
+ methods = ["GET"],
129
+ } = options;
130
+
131
+ let cacheAdapter: CacheAdapter;
132
+
133
+ if (redis) {
134
+ const redisAdapter = createRedisAdapter({
135
+ ...redis,
136
+ ttl: Math.ceil(ttl / 1000),
137
+ });
138
+ cacheAdapter = redisAdapter || new LRUCache({ maxSize, ttl });
139
+ } else {
140
+ cacheAdapter = new LRUCache({ maxSize, ttl });
141
+ }
142
+
143
+ function shouldExclude(url: string): boolean {
144
+ return exclude.some((pattern) => {
145
+ if (pattern instanceof RegExp) return pattern.test(url);
146
+ return url.includes(pattern);
147
+ });
148
+ }
149
+
150
+ function getCacheKey(req: Request): string {
151
+ return `${req.method}:${req.originalUrl || req.url}`;
152
+ }
153
+
154
+ const handler = async (
155
+ req: Request,
156
+ res: Response,
157
+ next: NextFunction,
158
+ ): Promise<void> => {
159
+ // Only cache specified methods
160
+ if (!methods.includes(req.method)) {
161
+ return next();
162
+ }
163
+
164
+ // Check exclusion patterns
165
+ const url = req.originalUrl || req.url;
166
+ if (shouldExclude(url)) {
167
+ return next();
168
+ }
169
+
170
+ const key = getCacheKey(req);
171
+
172
+ try {
173
+ const cached = await cacheAdapter.get(key);
174
+ if (cached) {
175
+ if (store) store.recordCacheHit();
176
+ const entry = cached as CacheEntry;
177
+
178
+ res.set("X-Cache", "HIT");
179
+ res.set("Content-Type", entry.contentType || "application/json");
180
+ res.status(entry.statusCode || 200).send(entry.body);
181
+ return;
182
+ }
183
+ } catch {
184
+ // Cache read failed — continue to handler
185
+ }
186
+
187
+ if (store) store.recordCacheMiss();
188
+ res.set("X-Cache", "MISS");
189
+
190
+ // Intercept response to cache it
191
+ const originalSend = res.send.bind(res);
192
+ res.send = function (body: unknown): Response {
193
+ // Only cache successful responses
194
+ if (res.statusCode >= 200 && res.statusCode < 400) {
195
+ const entry: CacheEntry = {
196
+ body: body as string | Buffer,
197
+ statusCode: res.statusCode,
198
+ contentType: res.get("Content-Type"),
199
+ };
200
+
201
+ try {
202
+ cacheAdapter.set(key, entry);
203
+ if (
204
+ store &&
205
+ typeof cacheAdapter.size === "number" &&
206
+ cacheAdapter.size >= 0
207
+ ) {
208
+ store.setCacheSize(cacheAdapter.size);
209
+ }
210
+ } catch {
211
+ // Cache write failed — ignore
212
+ }
213
+ }
214
+
215
+ return originalSend(body);
216
+ };
217
+
218
+ next();
219
+ };
220
+
221
+ // Build the CacheMiddleware with attached control methods
222
+ const middleware = handler as unknown as CacheMiddleware;
223
+ middleware.clear = () => cacheAdapter.clear();
224
+ middleware.delete = (key: string) => cacheAdapter.delete(key);
225
+ middleware.adapter = cacheAdapter;
226
+
227
+ return middleware;
228
+ }
@@ -0,0 +1,25 @@
1
+ import { Request, Response, NextFunction } from 'express';
2
+ import compressionMiddleware from 'compression';
3
+ import { CompressionOptions } from './types';
4
+
5
+ /**
6
+ * Create compression middleware with sensible defaults.
7
+ */
8
+ export function createCompressionMiddleware(
9
+ options: CompressionOptions = {}
10
+ ): (req: Request, res: Response, next: NextFunction) => void {
11
+ const { threshold = 1024, level = 6 } = options;
12
+
13
+ return compressionMiddleware({
14
+ threshold,
15
+ level,
16
+ filter: (req: Request, res: Response): boolean => {
17
+ // Don't compress if the client didn't request it
18
+ if (req.headers['x-no-compression']) {
19
+ return false;
20
+ }
21
+ // Use compression's default filter
22
+ return compressionMiddleware.filter(req, res);
23
+ },
24
+ });
25
+ }