@engjts/nexus 0.1.8 → 0.1.9

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 (205) hide show
  1. package/package.json +1 -1
  2. package/BENCHMARK_REPORT.md +0 -343
  3. package/documentation/01-getting-started.md +0 -240
  4. package/documentation/02-context.md +0 -335
  5. package/documentation/03-routing.md +0 -397
  6. package/documentation/04-middleware.md +0 -483
  7. package/documentation/05-validation.md +0 -514
  8. package/documentation/06-error-handling.md +0 -465
  9. package/documentation/07-performance.md +0 -364
  10. package/documentation/08-adapters.md +0 -470
  11. package/documentation/09-api-reference.md +0 -548
  12. package/documentation/10-examples.md +0 -582
  13. package/documentation/11-deployment.md +0 -477
  14. package/documentation/12-sentry.md +0 -620
  15. package/documentation/13-sentry-data-storage.md +0 -996
  16. package/documentation/14-sentry-data-reference.md +0 -457
  17. package/documentation/15-sentry-summary.md +0 -409
  18. package/documentation/16-alerts-system.md +0 -745
  19. package/documentation/17-alert-adapters.md +0 -696
  20. package/documentation/18-alerts-implementation-summary.md +0 -385
  21. package/documentation/19-class-based-routing.md +0 -840
  22. package/documentation/20-websocket-realtime.md +0 -813
  23. package/documentation/21-cache-system.md +0 -510
  24. package/documentation/22-job-queue.md +0 -772
  25. package/documentation/23-sentry-plugin.md +0 -551
  26. package/documentation/24-testing-utilities.md +0 -1287
  27. package/documentation/25-api-versioning.md +0 -533
  28. package/documentation/26-context-store.md +0 -607
  29. package/documentation/27-dependency-injection.md +0 -329
  30. package/documentation/28-lifecycle-hooks.md +0 -521
  31. package/documentation/29-package-structure.md +0 -196
  32. package/documentation/30-plugin-system.md +0 -414
  33. package/documentation/31-jwt-authentication.md +0 -597
  34. package/documentation/32-cli.md +0 -268
  35. package/documentation/ALERTS-COMPLETE-SUMMARY.md +0 -429
  36. package/documentation/ALERTS-INDEX.md +0 -330
  37. package/documentation/ALERTS-QUICK-REFERENCE.md +0 -286
  38. package/documentation/README.md +0 -178
  39. package/documentation/index.html +0 -34
  40. package/modern_framework_paper.md +0 -1870
  41. package/public/css/style.css +0 -87
  42. package/public/index.html +0 -34
  43. package/public/js/app.js +0 -27
  44. package/src/advanced/cache/InMemoryCacheStore.ts +0 -68
  45. package/src/advanced/cache/MultiTierCache.ts +0 -194
  46. package/src/advanced/cache/RedisCacheStore.ts +0 -341
  47. package/src/advanced/cache/index.ts +0 -5
  48. package/src/advanced/cache/types.ts +0 -40
  49. package/src/advanced/graphql/SimpleDataLoader.ts +0 -42
  50. package/src/advanced/graphql/index.ts +0 -22
  51. package/src/advanced/graphql/server.ts +0 -252
  52. package/src/advanced/graphql/types.ts +0 -42
  53. package/src/advanced/jobs/InMemoryQueueStore.ts +0 -68
  54. package/src/advanced/jobs/JobQueue.ts +0 -556
  55. package/src/advanced/jobs/RedisQueueStore.ts +0 -367
  56. package/src/advanced/jobs/index.ts +0 -5
  57. package/src/advanced/jobs/types.ts +0 -70
  58. package/src/advanced/observability/APMManager.ts +0 -163
  59. package/src/advanced/observability/AlertManager.ts +0 -109
  60. package/src/advanced/observability/MetricRegistry.ts +0 -151
  61. package/src/advanced/observability/ObservabilityCenter.ts +0 -304
  62. package/src/advanced/observability/StructuredLogger.ts +0 -154
  63. package/src/advanced/observability/TracingManager.ts +0 -117
  64. package/src/advanced/observability/adapters.ts +0 -304
  65. package/src/advanced/observability/createObservabilityMiddleware.ts +0 -63
  66. package/src/advanced/observability/index.ts +0 -11
  67. package/src/advanced/observability/types.ts +0 -174
  68. package/src/advanced/playground/extractPathParams.ts +0 -6
  69. package/src/advanced/playground/generateFieldExample.ts +0 -31
  70. package/src/advanced/playground/generatePlaygroundHTML.ts +0 -1956
  71. package/src/advanced/playground/generateSummary.ts +0 -19
  72. package/src/advanced/playground/getTagFromPath.ts +0 -9
  73. package/src/advanced/playground/index.ts +0 -8
  74. package/src/advanced/playground/playground.ts +0 -250
  75. package/src/advanced/playground/types.ts +0 -49
  76. package/src/advanced/playground/zodToExample.ts +0 -16
  77. package/src/advanced/playground/zodToParams.ts +0 -15
  78. package/src/advanced/postman/buildAuth.ts +0 -31
  79. package/src/advanced/postman/buildBody.ts +0 -15
  80. package/src/advanced/postman/buildQueryParams.ts +0 -27
  81. package/src/advanced/postman/buildRequestItem.ts +0 -36
  82. package/src/advanced/postman/buildResponses.ts +0 -11
  83. package/src/advanced/postman/buildUrl.ts +0 -33
  84. package/src/advanced/postman/capitalize.ts +0 -4
  85. package/src/advanced/postman/generateCollection.ts +0 -59
  86. package/src/advanced/postman/generateEnvironment.ts +0 -34
  87. package/src/advanced/postman/generateExampleFromZod.ts +0 -21
  88. package/src/advanced/postman/generateFieldExample.ts +0 -45
  89. package/src/advanced/postman/generateName.ts +0 -20
  90. package/src/advanced/postman/generateUUID.ts +0 -11
  91. package/src/advanced/postman/getTagFromPath.ts +0 -10
  92. package/src/advanced/postman/index.ts +0 -28
  93. package/src/advanced/postman/postman.ts +0 -156
  94. package/src/advanced/postman/slugify.ts +0 -7
  95. package/src/advanced/postman/types.ts +0 -140
  96. package/src/advanced/realtime/index.ts +0 -18
  97. package/src/advanced/realtime/websocket.ts +0 -231
  98. package/src/advanced/sentry/index.ts +0 -1236
  99. package/src/advanced/sentry/types.ts +0 -355
  100. package/src/advanced/static/generateDirectoryListing.ts +0 -47
  101. package/src/advanced/static/generateETag.ts +0 -7
  102. package/src/advanced/static/getMimeType.ts +0 -9
  103. package/src/advanced/static/index.ts +0 -32
  104. package/src/advanced/static/isSafePath.ts +0 -13
  105. package/src/advanced/static/publicDir.ts +0 -21
  106. package/src/advanced/static/serveStatic.ts +0 -225
  107. package/src/advanced/static/spa.ts +0 -24
  108. package/src/advanced/static/types.ts +0 -159
  109. package/src/advanced/swagger/SwaggerGenerator.ts +0 -66
  110. package/src/advanced/swagger/buildOperation.ts +0 -61
  111. package/src/advanced/swagger/buildParameters.ts +0 -61
  112. package/src/advanced/swagger/buildRequestBody.ts +0 -21
  113. package/src/advanced/swagger/buildResponses.ts +0 -54
  114. package/src/advanced/swagger/capitalize.ts +0 -5
  115. package/src/advanced/swagger/convertPath.ts +0 -9
  116. package/src/advanced/swagger/createSwagger.ts +0 -12
  117. package/src/advanced/swagger/generateOperationId.ts +0 -21
  118. package/src/advanced/swagger/generateSpec.ts +0 -105
  119. package/src/advanced/swagger/generateSummary.ts +0 -24
  120. package/src/advanced/swagger/generateSwaggerUI.ts +0 -70
  121. package/src/advanced/swagger/generateThemeCss.ts +0 -53
  122. package/src/advanced/swagger/index.ts +0 -25
  123. package/src/advanced/swagger/swagger.ts +0 -237
  124. package/src/advanced/swagger/types.ts +0 -206
  125. package/src/advanced/swagger/zodFieldToOpenAPI.ts +0 -94
  126. package/src/advanced/swagger/zodSchemaToOpenAPI.ts +0 -50
  127. package/src/advanced/swagger/zodToOpenAPI.ts +0 -22
  128. package/src/advanced/testing/factory.ts +0 -509
  129. package/src/advanced/testing/harness.ts +0 -612
  130. package/src/advanced/testing/index.ts +0 -430
  131. package/src/advanced/testing/load-test.ts +0 -618
  132. package/src/advanced/testing/mock-server.ts +0 -498
  133. package/src/advanced/testing/mock.ts +0 -670
  134. package/src/cli/bin.ts +0 -9
  135. package/src/cli/cli.ts +0 -158
  136. package/src/cli/commands/add.ts +0 -178
  137. package/src/cli/commands/build.ts +0 -73
  138. package/src/cli/commands/create.ts +0 -166
  139. package/src/cli/commands/dev.ts +0 -85
  140. package/src/cli/commands/generate.ts +0 -99
  141. package/src/cli/commands/help.ts +0 -95
  142. package/src/cli/commands/init.ts +0 -91
  143. package/src/cli/commands/version.ts +0 -38
  144. package/src/cli/index.ts +0 -6
  145. package/src/cli/templates/generators.ts +0 -359
  146. package/src/cli/templates/index.ts +0 -680
  147. package/src/cli/utils/exec.ts +0 -52
  148. package/src/cli/utils/file-system.ts +0 -78
  149. package/src/cli/utils/logger.ts +0 -111
  150. package/src/core/adapter.ts +0 -88
  151. package/src/core/application.ts +0 -1453
  152. package/src/core/context-pool.ts +0 -79
  153. package/src/core/context.ts +0 -856
  154. package/src/core/index.ts +0 -94
  155. package/src/core/middleware.ts +0 -272
  156. package/src/core/performance/buffer-pool.ts +0 -108
  157. package/src/core/performance/middleware-optimizer.ts +0 -162
  158. package/src/core/plugin/PluginManager.ts +0 -435
  159. package/src/core/plugin/builder.ts +0 -358
  160. package/src/core/plugin/index.ts +0 -50
  161. package/src/core/plugin/types.ts +0 -214
  162. package/src/core/router/file-router.ts +0 -623
  163. package/src/core/router/index.ts +0 -260
  164. package/src/core/router/radix-tree.ts +0 -242
  165. package/src/core/serializer.ts +0 -397
  166. package/src/core/store/index.ts +0 -30
  167. package/src/core/store/registry.ts +0 -178
  168. package/src/core/store/request-store.ts +0 -240
  169. package/src/core/store/types.ts +0 -233
  170. package/src/core/types.ts +0 -616
  171. package/src/database/adapter.ts +0 -35
  172. package/src/database/adapters/index.ts +0 -1
  173. package/src/database/adapters/mysql.ts +0 -669
  174. package/src/database/database.ts +0 -70
  175. package/src/database/dialect.ts +0 -388
  176. package/src/database/index.ts +0 -12
  177. package/src/database/migrations.ts +0 -86
  178. package/src/database/optimizer.ts +0 -125
  179. package/src/database/query-builder.ts +0 -404
  180. package/src/database/realtime.ts +0 -53
  181. package/src/database/schema.ts +0 -71
  182. package/src/database/transactions.ts +0 -56
  183. package/src/database/types.ts +0 -87
  184. package/src/deployment/cluster.ts +0 -471
  185. package/src/deployment/config.ts +0 -454
  186. package/src/deployment/docker.ts +0 -599
  187. package/src/deployment/graceful-shutdown.ts +0 -373
  188. package/src/deployment/index.ts +0 -56
  189. package/src/index.ts +0 -281
  190. package/src/security/adapter.ts +0 -318
  191. package/src/security/auth/JWTPlugin.ts +0 -234
  192. package/src/security/auth/JWTProvider.ts +0 -316
  193. package/src/security/auth/adapter.ts +0 -12
  194. package/src/security/auth/jwt.ts +0 -234
  195. package/src/security/auth/middleware.ts +0 -188
  196. package/src/security/csrf.ts +0 -220
  197. package/src/security/headers.ts +0 -108
  198. package/src/security/index.ts +0 -60
  199. package/src/security/rate-limit/adapter.ts +0 -7
  200. package/src/security/rate-limit/memory.ts +0 -108
  201. package/src/security/rate-limit/middleware.ts +0 -181
  202. package/src/security/sanitization.ts +0 -75
  203. package/src/security/types.ts +0 -240
  204. package/src/security/utils.ts +0 -52
  205. package/tsconfig.json +0 -39
@@ -1,618 +0,0 @@
1
- /**
2
- * Load testing utilities for stress testing APIs
3
- */
4
-
5
- import { request as httpRequest, RequestOptions, IncomingMessage } from 'http';
6
- import { request as httpsRequest } from 'https';
7
- import { URL } from 'url';
8
- import { EventEmitter } from 'events';
9
-
10
- export interface LoadTestScenario {
11
- name: string;
12
- executor: 'constant-vus' | 'ramping-vus' | 'constant-rate' | 'ramping-rate';
13
- vus?: number;
14
- duration: string;
15
- startVUs?: number;
16
- stages?: Array<{ duration: string; target: number }>;
17
- rate?: number;
18
- exec: string;
19
- }
20
-
21
- export interface LoadTestThresholds {
22
- [metric: string]: string[];
23
- }
24
-
25
- export interface LoadTestOptions {
26
- baseUrl: string;
27
- duration?: string;
28
- vus?: number;
29
- scenarios?: Record<string, LoadTestScenario>;
30
- thresholds?: LoadTestThresholds;
31
- setupTimeout?: number;
32
- teardownTimeout?: number;
33
- }
34
-
35
- export interface LoadTestResult {
36
- totalRequests: number;
37
- successfulRequests: number;
38
- failedRequests: number;
39
- duration: number;
40
- requestsPerSecond: number;
41
- latency: {
42
- min: number;
43
- max: number;
44
- avg: number;
45
- p50: number;
46
- p90: number;
47
- p95: number;
48
- p99: number;
49
- };
50
- errorRate: number;
51
- thresholdsPassed: boolean;
52
- thresholdResults: Record<string, { passed: boolean; value: number; threshold: string }>;
53
- errors: Array<{ message: string; count: number }>;
54
- }
55
-
56
- export interface VirtualUser {
57
- id: number;
58
- iteration: number;
59
- data: Record<string, any>;
60
- }
61
-
62
- type TestFunction = (vu: VirtualUser, http: HttpClient) => Promise<void>;
63
-
64
- /**
65
- * Simple HTTP client for load testing
66
- */
67
- export class HttpClient {
68
- private baseUrl: string;
69
- private defaultHeaders: Record<string, string> = {};
70
- private latencies: number[] = [];
71
- private errors: Map<string, number> = new Map();
72
- private successCount = 0;
73
- private failCount = 0;
74
-
75
- constructor(baseUrl: string) {
76
- this.baseUrl = baseUrl;
77
- }
78
-
79
- setHeader(key: string, value: string): void {
80
- this.defaultHeaders[key] = value;
81
- }
82
-
83
- async get(path: string, options: { headers?: Record<string, string>; timeout?: number } = {}): Promise<HttpResponse> {
84
- return this.request('GET', path, undefined, options);
85
- }
86
-
87
- async post(path: string, body?: any, options: { headers?: Record<string, string>; timeout?: number } = {}): Promise<HttpResponse> {
88
- return this.request('POST', path, body, options);
89
- }
90
-
91
- async put(path: string, body?: any, options: { headers?: Record<string, string>; timeout?: number } = {}): Promise<HttpResponse> {
92
- return this.request('PUT', path, body, options);
93
- }
94
-
95
- async delete(path: string, options: { headers?: Record<string, string>; timeout?: number } = {}): Promise<HttpResponse> {
96
- return this.request('DELETE', path, undefined, options);
97
- }
98
-
99
- private async request(
100
- method: string,
101
- path: string,
102
- body?: any,
103
- options: { headers?: Record<string, string>; timeout?: number } = {}
104
- ): Promise<HttpResponse> {
105
- const url = new URL(path, this.baseUrl);
106
- const isHttps = url.protocol === 'https:';
107
- const requester = isHttps ? httpsRequest : httpRequest;
108
-
109
- const bodyString = body ? JSON.stringify(body) : undefined;
110
- const headers: Record<string, string> = {
111
- ...this.defaultHeaders,
112
- ...options.headers
113
- };
114
-
115
- if (bodyString) {
116
- headers['Content-Type'] = 'application/json';
117
- headers['Content-Length'] = Buffer.byteLength(bodyString).toString();
118
- }
119
-
120
- const requestOptions: RequestOptions = {
121
- method,
122
- hostname: url.hostname,
123
- port: url.port || (isHttps ? 443 : 80),
124
- path: url.pathname + url.search,
125
- headers,
126
- timeout: options.timeout ?? 30000
127
- };
128
-
129
- const start = process.hrtime.bigint();
130
-
131
- try {
132
- const response = await new Promise<HttpResponse>((resolve, reject) => {
133
- const req = requester(requestOptions, (res: IncomingMessage) => {
134
- const chunks: Buffer[] = [];
135
- res.on('data', chunk => chunks.push(chunk));
136
- res.on('end', () => {
137
- const latency = Number(process.hrtime.bigint() - start) / 1e6;
138
- this.latencies.push(latency);
139
-
140
- const responseBody = Buffer.concat(chunks).toString('utf-8');
141
- const status = res.statusCode ?? 0;
142
-
143
- if (status >= 400) {
144
- this.failCount++;
145
- const errorKey = `HTTP ${status}`;
146
- this.errors.set(errorKey, (this.errors.get(errorKey) ?? 0) + 1);
147
- } else {
148
- this.successCount++;
149
- }
150
-
151
- resolve({
152
- status,
153
- headers: res.headers as Record<string, string | string[]>,
154
- body: responseBody,
155
- latency
156
- });
157
- });
158
- });
159
-
160
- req.on('error', (error) => {
161
- this.failCount++;
162
- const errorKey = error.message;
163
- this.errors.set(errorKey, (this.errors.get(errorKey) ?? 0) + 1);
164
- reject(error);
165
- });
166
-
167
- req.on('timeout', () => {
168
- req.destroy();
169
- this.failCount++;
170
- const errorKey = 'Request timeout';
171
- this.errors.set(errorKey, (this.errors.get(errorKey) ?? 0) + 1);
172
- reject(new Error('Request timeout'));
173
- });
174
-
175
- if (bodyString) {
176
- req.write(bodyString);
177
- }
178
- req.end();
179
- });
180
-
181
- return response;
182
- } catch (error) {
183
- throw error;
184
- }
185
- }
186
-
187
- getStats() {
188
- return {
189
- latencies: this.latencies,
190
- errors: this.errors,
191
- successCount: this.successCount,
192
- failCount: this.failCount
193
- };
194
- }
195
-
196
- reset() {
197
- this.latencies = [];
198
- this.errors.clear();
199
- this.successCount = 0;
200
- this.failCount = 0;
201
- }
202
- }
203
-
204
- export interface HttpResponse {
205
- status: number;
206
- headers: Record<string, string | string[]>;
207
- body: string;
208
- latency: number;
209
- }
210
-
211
- /**
212
- * Parse duration string to milliseconds
213
- */
214
- function parseDuration(duration: string): number {
215
- const match = duration.match(/^(\d+)(s|m|h)$/);
216
- if (!match) {
217
- throw new Error(`Invalid duration format: ${duration}`);
218
- }
219
-
220
- const value = parseInt(match[1], 10);
221
- const unit = match[2];
222
-
223
- switch (unit) {
224
- case 's': return value * 1000;
225
- case 'm': return value * 60 * 1000;
226
- case 'h': return value * 60 * 60 * 1000;
227
- default: throw new Error(`Unknown duration unit: ${unit}`);
228
- }
229
- }
230
-
231
- /**
232
- * Calculate percentile from sorted array
233
- */
234
- function percentile(sorted: number[], p: number): number {
235
- if (sorted.length === 0) return 0;
236
- const index = Math.ceil((p / 100) * sorted.length) - 1;
237
- return sorted[Math.max(0, index)];
238
- }
239
-
240
- /**
241
- * Load test runner
242
- */
243
- export class LoadTestRunner extends EventEmitter {
244
- private options: LoadTestOptions;
245
- private tests: Map<string, TestFunction> = new Map();
246
- private running = false;
247
- private allLatencies: number[] = [];
248
- private allErrors: Map<string, number> = new Map();
249
- private totalSuccess = 0;
250
- private totalFail = 0;
251
-
252
- constructor(options: LoadTestOptions) {
253
- super();
254
- this.options = options;
255
- }
256
-
257
- /**
258
- * Register a test function
259
- */
260
- test(name: string, fn: TestFunction): this {
261
- this.tests.set(name, fn);
262
- return this;
263
- }
264
-
265
- /**
266
- * Run the load test
267
- */
268
- async run(): Promise<LoadTestResult> {
269
- this.running = true;
270
- this.allLatencies = [];
271
- this.allErrors.clear();
272
- this.totalSuccess = 0;
273
- this.totalFail = 0;
274
-
275
- const startTime = Date.now();
276
-
277
- // Simple mode: just VUs and duration
278
- if (!this.options.scenarios) {
279
- const vus = this.options.vus ?? 1;
280
- const duration = parseDuration(this.options.duration ?? '30s');
281
-
282
- const defaultTest = this.tests.get('default') || this.tests.values().next().value;
283
- if (!defaultTest) {
284
- throw new Error('No test function registered');
285
- }
286
-
287
- await this.runConstantVUs(vus, duration, defaultTest);
288
- } else {
289
- // Scenarios mode
290
- const scenarioPromises: Promise<void>[] = [];
291
-
292
- for (const [_name, scenario] of Object.entries(this.options.scenarios)) {
293
- const testFn = this.tests.get(scenario.exec);
294
- if (!testFn) {
295
- throw new Error(`Test function not found: ${scenario.exec}`);
296
- }
297
-
298
- switch (scenario.executor) {
299
- case 'constant-vus':
300
- scenarioPromises.push(
301
- this.runConstantVUs(scenario.vus ?? 1, parseDuration(scenario.duration), testFn)
302
- );
303
- break;
304
- case 'ramping-vus':
305
- if (scenario.stages) {
306
- scenarioPromises.push(
307
- this.runRampingVUs(scenario.startVUs ?? 0, scenario.stages, testFn)
308
- );
309
- }
310
- break;
311
- case 'constant-rate':
312
- scenarioPromises.push(
313
- this.runConstantRate(scenario.rate ?? 1, parseDuration(scenario.duration), testFn)
314
- );
315
- break;
316
- }
317
- }
318
-
319
- await Promise.all(scenarioPromises);
320
- }
321
-
322
- this.running = false;
323
- const totalDuration = Date.now() - startTime;
324
-
325
- return this.calculateResults(totalDuration);
326
- }
327
-
328
- /**
329
- * Stop the test
330
- */
331
- stop(): void {
332
- this.running = false;
333
- }
334
-
335
- private async runConstantVUs(vus: number, durationMs: number, testFn: TestFunction): Promise<void> {
336
- const endTime = Date.now() + durationMs;
337
- const vuPromises: Promise<void>[] = [];
338
-
339
- for (let i = 0; i < vus; i++) {
340
- vuPromises.push(this.runVU(i, endTime, testFn));
341
- }
342
-
343
- await Promise.all(vuPromises);
344
- }
345
-
346
- private async runRampingVUs(
347
- startVUs: number,
348
- stages: Array<{ duration: string; target: number }>,
349
- testFn: TestFunction
350
- ): Promise<void> {
351
- let currentVUs = startVUs;
352
- const activeVUs: Map<number, { stop: boolean }> = new Map();
353
- let vuIdCounter = 0;
354
-
355
- for (const stage of stages) {
356
- const stageDuration = parseDuration(stage.duration);
357
- const targetVUs = stage.target;
358
- const stageStart = Date.now();
359
- const stageEnd = stageStart + stageDuration;
360
-
361
- // Adjust VUs linearly over the stage
362
- const vuDiff = targetVUs - currentVUs;
363
- const interval = stageDuration / Math.abs(vuDiff || 1);
364
-
365
- while (Date.now() < stageEnd && this.running) {
366
- const elapsed = Date.now() - stageStart;
367
- const progress = elapsed / stageDuration;
368
- const desiredVUs = Math.round(currentVUs + vuDiff * progress);
369
-
370
- // Add or remove VUs
371
- while (activeVUs.size < desiredVUs && this.running) {
372
- const id = vuIdCounter++;
373
- const control = { stop: false };
374
- activeVUs.set(id, control);
375
- this.runVU(id, stageEnd, testFn, control);
376
- }
377
-
378
- while (activeVUs.size > desiredVUs) {
379
- const entry = activeVUs.entries().next().value;
380
- if (!entry) continue;
381
- const [id, control] = entry;
382
- control.stop = true;
383
- activeVUs.delete(id);
384
- }
385
-
386
- await this.sleep(Math.min(interval, 1000));
387
- }
388
-
389
- currentVUs = targetVUs;
390
- }
391
-
392
- // Stop all remaining VUs
393
- for (const control of activeVUs.values()) {
394
- control.stop = true;
395
- }
396
- }
397
-
398
- private async runConstantRate(rate: number, durationMs: number, testFn: TestFunction): Promise<void> {
399
- const endTime = Date.now() + durationMs;
400
- const interval = 1000 / rate;
401
- let vuId = 0;
402
-
403
- while (Date.now() < endTime && this.running) {
404
- const http = new HttpClient(this.options.baseUrl);
405
- const vu: VirtualUser = { id: vuId++, iteration: 0, data: {} };
406
-
407
- // Fire and forget
408
- testFn(vu, http)
409
- .then(() => this.collectStats(http))
410
- .catch(() => this.collectStats(http));
411
-
412
- await this.sleep(interval);
413
- }
414
- }
415
-
416
- private async runVU(
417
- id: number,
418
- endTime: number,
419
- testFn: TestFunction,
420
- control?: { stop: boolean }
421
- ): Promise<void> {
422
- const http = new HttpClient(this.options.baseUrl);
423
- const vu: VirtualUser = { id, iteration: 0, data: {} };
424
-
425
- while (Date.now() < endTime && this.running && !control?.stop) {
426
- try {
427
- await testFn(vu, http);
428
- } catch (error) {
429
- // Errors are already tracked in HttpClient
430
- }
431
- vu.iteration++;
432
- }
433
-
434
- this.collectStats(http);
435
- }
436
-
437
- private collectStats(http: HttpClient): void {
438
- const stats = http.getStats();
439
- this.allLatencies.push(...stats.latencies);
440
- this.totalSuccess += stats.successCount;
441
- this.totalFail += stats.failCount;
442
-
443
- for (const [error, count] of stats.errors) {
444
- this.allErrors.set(error, (this.allErrors.get(error) ?? 0) + count);
445
- }
446
- }
447
-
448
- private calculateResults(durationMs: number): LoadTestResult {
449
- const sorted = [...this.allLatencies].sort((a, b) => a - b);
450
- const total = this.totalSuccess + this.totalFail;
451
-
452
- const latencyStats = {
453
- min: sorted[0] ?? 0,
454
- max: sorted[sorted.length - 1] ?? 0,
455
- avg: sorted.length > 0 ? sorted.reduce((a, b) => a + b, 0) / sorted.length : 0,
456
- p50: percentile(sorted, 50),
457
- p90: percentile(sorted, 90),
458
- p95: percentile(sorted, 95),
459
- p99: percentile(sorted, 99)
460
- };
461
-
462
- const thresholdResults: Record<string, { passed: boolean; value: number; threshold: string }> = {};
463
- let allThresholdsPassed = true;
464
-
465
- if (this.options.thresholds) {
466
- for (const [metric, conditions] of Object.entries(this.options.thresholds)) {
467
- for (const condition of conditions) {
468
- const { passed, value } = this.evaluateThreshold(metric, condition, {
469
- latency: latencyStats,
470
- errorRate: total > 0 ? this.totalFail / total : 0,
471
- requestsPerSecond: total / (durationMs / 1000)
472
- });
473
-
474
- thresholdResults[`${metric}: ${condition}`] = { passed, value, threshold: condition };
475
- if (!passed) {
476
- allThresholdsPassed = false;
477
- }
478
- }
479
- }
480
- }
481
-
482
- return {
483
- totalRequests: total,
484
- successfulRequests: this.totalSuccess,
485
- failedRequests: this.totalFail,
486
- duration: durationMs,
487
- requestsPerSecond: total / (durationMs / 1000),
488
- latency: latencyStats,
489
- errorRate: total > 0 ? this.totalFail / total : 0,
490
- thresholdsPassed: allThresholdsPassed,
491
- thresholdResults,
492
- errors: Array.from(this.allErrors.entries()).map(([message, count]) => ({ message, count }))
493
- };
494
- }
495
-
496
- private evaluateThreshold(
497
- metric: string,
498
- condition: string,
499
- stats: { latency: LoadTestResult['latency']; errorRate: number; requestsPerSecond: number }
500
- ): { passed: boolean; value: number } {
501
- // Parse condition like "p(95)<500" or "rate<0.01" or "rate>100"
502
- const match = condition.match(/^(p\((\d+)\)|rate|avg|min|max)\s*([<>]=?)\s*(\d+(?:\.\d+)?)$/);
503
- if (!match) {
504
- return { passed: true, value: 0 };
505
- }
506
-
507
- const [, metricType, percentileValue, operator, thresholdStr] = match;
508
- const threshold = parseFloat(thresholdStr);
509
- let value: number;
510
-
511
- if (metric === 'http_req_duration') {
512
- if (metricType.startsWith('p(')) {
513
- const p = parseInt(percentileValue, 10);
514
- switch (p) {
515
- case 50: value = stats.latency.p50; break;
516
- case 90: value = stats.latency.p90; break;
517
- case 95: value = stats.latency.p95; break;
518
- case 99: value = stats.latency.p99; break;
519
- default: value = stats.latency.avg;
520
- }
521
- } else if (metricType === 'avg') {
522
- value = stats.latency.avg;
523
- } else if (metricType === 'min') {
524
- value = stats.latency.min;
525
- } else if (metricType === 'max') {
526
- value = stats.latency.max;
527
- } else {
528
- value = stats.latency.avg;
529
- }
530
- } else if (metric === 'http_req_failed') {
531
- value = stats.errorRate;
532
- } else if (metric === 'http_reqs') {
533
- value = stats.requestsPerSecond;
534
- } else {
535
- value = 0;
536
- }
537
-
538
- let passed: boolean;
539
- switch (operator) {
540
- case '<': passed = value < threshold; break;
541
- case '<=': passed = value <= threshold; break;
542
- case '>': passed = value > threshold; break;
543
- case '>=': passed = value >= threshold; break;
544
- default: passed = true;
545
- }
546
-
547
- return { passed, value };
548
- }
549
-
550
- private sleep(ms: number): Promise<void> {
551
- return new Promise(resolve => setTimeout(resolve, ms));
552
- }
553
- }
554
-
555
- /**
556
- * Create a load test
557
- */
558
- export function createLoadTest(options: LoadTestOptions): LoadTestRunner {
559
- return new LoadTestRunner(options);
560
- }
561
-
562
- /**
563
- * Helper to format load test results
564
- */
565
- export function formatLoadTestResults(results: LoadTestResult): string {
566
- const lines: string[] = [
567
- '',
568
- '═══════════════════════════════════════════════════════════════',
569
- ' LOAD TEST RESULTS ',
570
- '═══════════════════════════════════════════════════════════════',
571
- '',
572
- ' Summary',
573
- ' ───────────────────────────────────────────────────────────',
574
- ` Total Requests: ${results.totalRequests}`,
575
- ` Successful: ${results.successfulRequests}`,
576
- ` Failed: ${results.failedRequests}`,
577
- ` Duration: ${(results.duration / 1000).toFixed(2)}s`,
578
- ` Requests/sec: ${results.requestsPerSecond.toFixed(2)}`,
579
- ` Error Rate: ${(results.errorRate * 100).toFixed(2)}%`,
580
- '',
581
- ' Latency',
582
- ' ───────────────────────────────────────────────────────────',
583
- ` Min: ${results.latency.min.toFixed(2)}ms`,
584
- ` Max: ${results.latency.max.toFixed(2)}ms`,
585
- ` Avg: ${results.latency.avg.toFixed(2)}ms`,
586
- ` P50 (median): ${results.latency.p50.toFixed(2)}ms`,
587
- ` P90: ${results.latency.p90.toFixed(2)}ms`,
588
- ` P95: ${results.latency.p95.toFixed(2)}ms`,
589
- ` P99: ${results.latency.p99.toFixed(2)}ms`,
590
- ''
591
- ];
592
-
593
- if (Object.keys(results.thresholdResults).length > 0) {
594
- lines.push(' Thresholds');
595
- lines.push(' ───────────────────────────────────────────────────────────');
596
- for (const [name, result] of Object.entries(results.thresholdResults)) {
597
- const status = result.passed ? '✓' : '✗';
598
- lines.push(` ${status} ${name} (actual: ${result.value.toFixed(2)})`);
599
- }
600
- lines.push('');
601
- lines.push(` Overall: ${results.thresholdsPassed ? '✓ PASSED' : '✗ FAILED'}`);
602
- lines.push('');
603
- }
604
-
605
- if (results.errors.length > 0) {
606
- lines.push(' Errors');
607
- lines.push(' ───────────────────────────────────────────────────────────');
608
- for (const error of results.errors) {
609
- lines.push(` ${error.message}: ${error.count}`);
610
- }
611
- lines.push('');
612
- }
613
-
614
- lines.push('═══════════════════════════════════════════════════════════════');
615
- lines.push('');
616
-
617
- return lines.join('\n');
618
- }