qhttpx 1.8.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 (197) hide show
  1. package/.eslintrc.json +22 -0
  2. package/.github/workflows/ci.yml +32 -0
  3. package/.github/workflows/npm-publish.yml +37 -0
  4. package/.github/workflows/release.yml +21 -0
  5. package/.prettierrc +7 -0
  6. package/CHANGELOG.md +145 -0
  7. package/LICENSE +21 -0
  8. package/README.md +343 -0
  9. package/dist/package.json +61 -0
  10. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  11. package/dist/src/benchmarks/quantam-users.js +56 -0
  12. package/dist/src/benchmarks/simple-json.js +58 -0
  13. package/dist/src/benchmarks/ultra-mode.js +122 -0
  14. package/dist/src/cli/index.js +200 -0
  15. package/dist/src/client/index.js +72 -0
  16. package/dist/src/core/batch.js +97 -0
  17. package/dist/src/core/body-parser.js +121 -0
  18. package/dist/src/core/buffer-pool.js +70 -0
  19. package/dist/src/core/config.js +50 -0
  20. package/dist/src/core/fusion.js +183 -0
  21. package/dist/src/core/logger.js +49 -0
  22. package/dist/src/core/metrics.js +111 -0
  23. package/dist/src/core/resources.js +25 -0
  24. package/dist/src/core/scheduler.js +85 -0
  25. package/dist/src/core/scope.js +68 -0
  26. package/dist/src/core/serializer.js +44 -0
  27. package/dist/src/core/server.js +905 -0
  28. package/dist/src/core/stream.js +71 -0
  29. package/dist/src/core/tasks.js +87 -0
  30. package/dist/src/core/types.js +19 -0
  31. package/dist/src/core/websocket.js +86 -0
  32. package/dist/src/core/worker-queue.js +73 -0
  33. package/dist/src/database/adapters/memory.js +90 -0
  34. package/dist/src/database/adapters/mongo.js +141 -0
  35. package/dist/src/database/adapters/postgres.js +111 -0
  36. package/dist/src/database/adapters/sqlite.js +42 -0
  37. package/dist/src/database/coalescer.js +134 -0
  38. package/dist/src/database/manager.js +87 -0
  39. package/dist/src/database/types.js +2 -0
  40. package/dist/src/index.js +61 -0
  41. package/dist/src/middleware/compression.js +133 -0
  42. package/dist/src/middleware/cors.js +66 -0
  43. package/dist/src/middleware/presets.js +33 -0
  44. package/dist/src/middleware/rate-limit.js +77 -0
  45. package/dist/src/middleware/security.js +69 -0
  46. package/dist/src/middleware/static.js +191 -0
  47. package/dist/src/openapi/generator.js +149 -0
  48. package/dist/src/router/radix-router.js +89 -0
  49. package/dist/src/router/radix-tree.js +81 -0
  50. package/dist/src/router/router.js +146 -0
  51. package/dist/src/testing/index.js +84 -0
  52. package/dist/src/utils/cookies.js +59 -0
  53. package/dist/src/utils/logger.js +45 -0
  54. package/dist/src/utils/signals.js +31 -0
  55. package/dist/src/utils/sse.js +32 -0
  56. package/dist/src/validation/index.js +19 -0
  57. package/dist/src/validation/simple.js +102 -0
  58. package/dist/src/validation/types.js +12 -0
  59. package/dist/src/validation/zod.js +18 -0
  60. package/dist/src/views/index.js +17 -0
  61. package/dist/src/views/types.js +2 -0
  62. package/dist/tests/adapters.test.js +106 -0
  63. package/dist/tests/batch.test.js +117 -0
  64. package/dist/tests/body-parser.test.js +52 -0
  65. package/dist/tests/compression-sse.test.js +87 -0
  66. package/dist/tests/cookies.test.js +63 -0
  67. package/dist/tests/cors.test.js +55 -0
  68. package/dist/tests/database.test.js +80 -0
  69. package/dist/tests/dx.test.js +64 -0
  70. package/dist/tests/ecosystem.test.js +133 -0
  71. package/dist/tests/features.test.js +47 -0
  72. package/dist/tests/fusion.test.js +92 -0
  73. package/dist/tests/http-basic.test.js +124 -0
  74. package/dist/tests/logger.test.js +33 -0
  75. package/dist/tests/middleware.test.js +109 -0
  76. package/dist/tests/observability.test.js +59 -0
  77. package/dist/tests/openapi.test.js +64 -0
  78. package/dist/tests/plugin.test.js +65 -0
  79. package/dist/tests/plugins.test.js +71 -0
  80. package/dist/tests/rate-limit.test.js +77 -0
  81. package/dist/tests/resources.test.js +44 -0
  82. package/dist/tests/scheduler.test.js +46 -0
  83. package/dist/tests/schema-routes.test.js +77 -0
  84. package/dist/tests/security.test.js +83 -0
  85. package/dist/tests/server-db.test.js +72 -0
  86. package/dist/tests/smoke.test.js +10 -0
  87. package/dist/tests/sqlite-fusion.test.js +92 -0
  88. package/dist/tests/static.test.js +102 -0
  89. package/dist/tests/stream.test.js +44 -0
  90. package/dist/tests/task-metrics.test.js +53 -0
  91. package/dist/tests/tasks.test.js +62 -0
  92. package/dist/tests/testing.test.js +47 -0
  93. package/dist/tests/validation.test.js +107 -0
  94. package/dist/tests/websocket.test.js +146 -0
  95. package/dist/vitest.config.js +9 -0
  96. package/docs/AEGIS.md +76 -0
  97. package/docs/BENCHMARKS.md +36 -0
  98. package/docs/CAPABILITIES.md +70 -0
  99. package/docs/CLI.md +43 -0
  100. package/docs/DATABASE.md +142 -0
  101. package/docs/ECOSYSTEM.md +146 -0
  102. package/docs/NEXT_STEPS.md +99 -0
  103. package/docs/OPENAPI.md +99 -0
  104. package/docs/PLUGINS.md +59 -0
  105. package/docs/REAL_WORLD_EXAMPLES.md +109 -0
  106. package/docs/ROADMAP.md +366 -0
  107. package/docs/VALIDATION.md +136 -0
  108. package/eslint.config.cjs +26 -0
  109. package/examples/api-server.ts +254 -0
  110. package/package.json +61 -0
  111. package/src/benchmarks/compare-frameworks.ts +149 -0
  112. package/src/benchmarks/quantam-users.ts +70 -0
  113. package/src/benchmarks/simple-json.ts +71 -0
  114. package/src/benchmarks/ultra-mode.ts +159 -0
  115. package/src/cli/index.ts +214 -0
  116. package/src/client/index.ts +93 -0
  117. package/src/core/batch.ts +110 -0
  118. package/src/core/body-parser.ts +151 -0
  119. package/src/core/buffer-pool.ts +96 -0
  120. package/src/core/config.ts +60 -0
  121. package/src/core/fusion.ts +210 -0
  122. package/src/core/logger.ts +70 -0
  123. package/src/core/metrics.ts +166 -0
  124. package/src/core/resources.ts +38 -0
  125. package/src/core/scheduler.ts +126 -0
  126. package/src/core/scope.ts +87 -0
  127. package/src/core/serializer.ts +41 -0
  128. package/src/core/server.ts +1113 -0
  129. package/src/core/stream.ts +111 -0
  130. package/src/core/tasks.ts +138 -0
  131. package/src/core/types.ts +178 -0
  132. package/src/core/websocket.ts +112 -0
  133. package/src/core/worker-queue.ts +90 -0
  134. package/src/database/adapters/memory.ts +99 -0
  135. package/src/database/adapters/mongo.ts +116 -0
  136. package/src/database/adapters/postgres.ts +86 -0
  137. package/src/database/adapters/sqlite.ts +44 -0
  138. package/src/database/coalescer.ts +153 -0
  139. package/src/database/manager.ts +97 -0
  140. package/src/database/types.ts +24 -0
  141. package/src/index.ts +42 -0
  142. package/src/middleware/compression.ts +147 -0
  143. package/src/middleware/cors.ts +98 -0
  144. package/src/middleware/presets.ts +50 -0
  145. package/src/middleware/rate-limit.ts +106 -0
  146. package/src/middleware/security.ts +109 -0
  147. package/src/middleware/static.ts +216 -0
  148. package/src/openapi/generator.ts +167 -0
  149. package/src/router/radix-router.ts +119 -0
  150. package/src/router/radix-tree.ts +106 -0
  151. package/src/router/router.ts +190 -0
  152. package/src/testing/index.ts +104 -0
  153. package/src/utils/cookies.ts +67 -0
  154. package/src/utils/logger.ts +59 -0
  155. package/src/utils/signals.ts +45 -0
  156. package/src/utils/sse.ts +41 -0
  157. package/src/validation/index.ts +3 -0
  158. package/src/validation/simple.ts +93 -0
  159. package/src/validation/types.ts +38 -0
  160. package/src/validation/zod.ts +14 -0
  161. package/src/views/index.ts +1 -0
  162. package/src/views/types.ts +4 -0
  163. package/tests/adapters.test.ts +120 -0
  164. package/tests/batch.test.ts +139 -0
  165. package/tests/body-parser.test.ts +83 -0
  166. package/tests/compression-sse.test.ts +98 -0
  167. package/tests/cookies.test.ts +74 -0
  168. package/tests/cors.test.ts +79 -0
  169. package/tests/database.test.ts +90 -0
  170. package/tests/dx.test.ts +78 -0
  171. package/tests/ecosystem.test.ts +156 -0
  172. package/tests/features.test.ts +51 -0
  173. package/tests/fusion.test.ts +121 -0
  174. package/tests/http-basic.test.ts +161 -0
  175. package/tests/logger.test.ts +48 -0
  176. package/tests/middleware.test.ts +137 -0
  177. package/tests/observability.test.ts +91 -0
  178. package/tests/openapi.test.ts +74 -0
  179. package/tests/plugin.test.ts +85 -0
  180. package/tests/plugins.test.ts +93 -0
  181. package/tests/rate-limit.test.ts +97 -0
  182. package/tests/resources.test.ts +64 -0
  183. package/tests/scheduler.test.ts +71 -0
  184. package/tests/schema-routes.test.ts +89 -0
  185. package/tests/security.test.ts +128 -0
  186. package/tests/server-db.test.ts +72 -0
  187. package/tests/smoke.test.ts +9 -0
  188. package/tests/sqlite-fusion.test.ts +106 -0
  189. package/tests/static.test.ts +111 -0
  190. package/tests/stream.test.ts +58 -0
  191. package/tests/task-metrics.test.ts +78 -0
  192. package/tests/tasks.test.ts +90 -0
  193. package/tests/testing.test.ts +53 -0
  194. package/tests/validation.test.ts +126 -0
  195. package/tests/websocket.test.ts +132 -0
  196. package/tsconfig.json +16 -0
  197. package/vitest.config.ts +9 -0
@@ -0,0 +1,137 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { QHTTPX } from '../src/index';
3
+
4
+ describe('QHTTPX middleware pipeline', () => {
5
+ let app: QHTTPX | undefined;
6
+
7
+ afterEach(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+
14
+ it('runs middleware in order with await next()', async () => {
15
+ app = new QHTTPX();
16
+
17
+ const calls: string[] = [];
18
+
19
+ app.use(async (ctx, next) => {
20
+ calls.push('m1-before');
21
+ await next();
22
+ calls.push('m1-after');
23
+ });
24
+
25
+ app.use(async (ctx, next) => {
26
+ calls.push('m2-before');
27
+ await next();
28
+ calls.push('m2-after');
29
+ });
30
+
31
+ app.get('/', (ctx) => {
32
+ calls.push('handler');
33
+ ctx.send('ok');
34
+ });
35
+
36
+ const { port } = await app.listen(0, '127.0.0.1');
37
+
38
+ const response = await fetch(`http://127.0.0.1:${port}/`);
39
+ expect(response.status).toBe(200);
40
+
41
+ expect(calls).toEqual([
42
+ 'm1-before',
43
+ 'm2-before',
44
+ 'handler',
45
+ 'm2-after',
46
+ 'm1-after',
47
+ ]);
48
+ });
49
+
50
+ it('allows middleware to modify response headers', async () => {
51
+ app = new QHTTPX();
52
+
53
+ app.use((ctx, next) => {
54
+ ctx.res.setHeader('x-powered-by', 'qhttpx');
55
+ return next();
56
+ });
57
+
58
+ app.get('/', (ctx) => {
59
+ ctx.send('ok');
60
+ });
61
+
62
+ const { port } = await app.listen(0, '127.0.0.1');
63
+
64
+ const response = await fetch(`http://127.0.0.1:${port}/`);
65
+ expect(response.status).toBe(200);
66
+ const header = response.headers.get('x-powered-by');
67
+ expect(header).toBe('qhttpx');
68
+ });
69
+
70
+ it('supports auth middleware that short-circuits unauthorized requests', async () => {
71
+ app = new QHTTPX();
72
+
73
+ app.use((ctx, next) => {
74
+ const auth = ctx.req.headers['authorization'];
75
+ if (auth !== 'Bearer secret') {
76
+ ctx.res.statusCode = 401;
77
+ ctx.res.end('Unauthorized');
78
+ return;
79
+ }
80
+ return next();
81
+ });
82
+
83
+ app.get('/secure', (ctx) => {
84
+ ctx.send('ok');
85
+ });
86
+
87
+ const { port } = await app.listen(0, '127.0.0.1');
88
+
89
+ const unauthorized = await fetch(`http://127.0.0.1:${port}/secure`);
90
+ expect(unauthorized.status).toBe(401);
91
+ const unauthorizedText = await unauthorized.text();
92
+ expect(unauthorizedText).toBe('Unauthorized');
93
+
94
+ const authorized = await fetch(`http://127.0.0.1:${port}/secure`, {
95
+ headers: {
96
+ authorization: 'Bearer secret',
97
+ },
98
+ });
99
+ expect(authorized.status).toBe(200);
100
+ const body = await authorized.text();
101
+ expect(body).toBe('ok');
102
+ });
103
+
104
+ it('supports error-handling middleware that converts errors to JSON', async () => {
105
+ app = new QHTTPX();
106
+
107
+ app.use(async (ctx, next) => {
108
+ try {
109
+ await next();
110
+ } catch (err) {
111
+ ctx.res.statusCode = 500;
112
+ ctx.res.setHeader('content-type', 'application/json; charset=utf-8');
113
+ ctx.res.end(
114
+ JSON.stringify({
115
+ error: 'internal',
116
+ message: err instanceof Error ? err.message : 'unknown',
117
+ }),
118
+ );
119
+ }
120
+ });
121
+
122
+ app.get('/boom', () => {
123
+ throw new Error('boom');
124
+ });
125
+
126
+ const { port } = await app.listen(0, '127.0.0.1');
127
+
128
+ const response = await fetch(`http://127.0.0.1:${port}/boom`);
129
+ expect(response.status).toBe(500);
130
+ const json = (await response.json()) as {
131
+ error: string;
132
+ message: string;
133
+ };
134
+ expect(json.error).toBe('internal');
135
+ expect(json.message).toBe('boom');
136
+ });
137
+ });
@@ -0,0 +1,91 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { QHTTPX } from '../src/index';
3
+
4
+ describe('QHTTPX observability and health', () => {
5
+ let app: QHTTPX | undefined;
6
+
7
+ afterEach(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+
14
+ it('exposes a health endpoint with basic info', async () => {
15
+ app = new QHTTPX();
16
+
17
+ const { port } = await app.listen(0, '127.0.0.1');
18
+
19
+ const response = await fetch(
20
+ `http://127.0.0.1:${port}/__qhttpx/health`,
21
+ );
22
+
23
+ expect(response.status).toBe(200);
24
+ const json = (await response.json()) as {
25
+ status: string;
26
+ name: string;
27
+ version: string;
28
+ workers: number;
29
+ };
30
+
31
+ expect(json.status).toBe('ok');
32
+ expect(typeof json.name).toBe('string');
33
+ expect(typeof json.version).toBe('string');
34
+ expect(json.workers).toBeGreaterThan(0);
35
+ });
36
+
37
+ it('exposes metrics for handled requests', async () => {
38
+ app = new QHTTPX();
39
+
40
+ app.get('/ping', (ctx) => {
41
+ ctx.send('pong');
42
+ });
43
+
44
+ const { port } = await app.listen(0, '127.0.0.1');
45
+
46
+ await fetch(`http://127.0.0.1:${port}/ping`);
47
+ await fetch(`http://127.0.0.1:${port}/ping`);
48
+
49
+ const response = await fetch(
50
+ `http://127.0.0.1:${port}/__qhttpx/metrics`,
51
+ );
52
+
53
+ expect(response.status).toBe(200);
54
+ const json = (await response.json()) as {
55
+ totalRequests: number;
56
+ inFlightRequests: number;
57
+ latency: { p50: number | null };
58
+ memory: { rssBytes: number; heapUsedBytes: number };
59
+ workers: number;
60
+ };
61
+
62
+ expect(json.totalRequests).toBeGreaterThanOrEqual(2);
63
+ expect(json.inFlightRequests).toBeGreaterThanOrEqual(0);
64
+ expect(json.latency.p50 === null || json.latency.p50 >= 0).toBe(true);
65
+ expect(json.memory.rssBytes).toBeGreaterThan(0);
66
+ expect(json.memory.heapUsedBytes).toBeGreaterThan(0);
67
+ expect(json.workers).toBeGreaterThan(0);
68
+ });
69
+
70
+ it('propagates and generates x-request-id headers', async () => {
71
+ app = new QHTTPX();
72
+
73
+ app.get('/ping', (ctx) => {
74
+ ctx.send('pong');
75
+ });
76
+
77
+ const { port } = await app.listen(0, '127.0.0.1');
78
+
79
+ const first = await fetch(`http://127.0.0.1:${port}/ping`);
80
+ const firstId = first.headers.get('x-request-id');
81
+ expect(firstId === null || firstId.length > 0).toBe(true);
82
+
83
+ const second = await fetch(`http://127.0.0.1:${port}/ping`, {
84
+ headers: {
85
+ 'x-request-id': 'custom-id-123',
86
+ },
87
+ });
88
+ const secondId = second.headers.get('x-request-id');
89
+ expect(secondId).toBe('custom-id-123');
90
+ });
91
+ });
@@ -0,0 +1,74 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { RouteSchema } from '../src/validation/types';
4
+
5
+ describe('OpenAPI Generator', () => {
6
+ it('should generate valid OpenAPI spec from routes', () => {
7
+ const app = new QHTTPX();
8
+
9
+ const userSchema: RouteSchema = {
10
+ params: {
11
+ type: 'object',
12
+ properties: {
13
+ id: { type: 'string' }
14
+ }
15
+ },
16
+ body: {
17
+ type: 'object',
18
+ properties: {
19
+ name: { type: 'string' },
20
+ age: { type: 'number', min: 18 }
21
+ }
22
+ },
23
+ response: {
24
+ type: 'object',
25
+ properties: {
26
+ success: { type: 'boolean' }
27
+ }
28
+ }
29
+ };
30
+
31
+ app.post('/users/:id', {
32
+ schema: userSchema,
33
+ handler: (ctx) => { ctx.json({ success: true }) }
34
+ });
35
+
36
+ app.get('/health', (ctx) => { ctx.send('ok') });
37
+
38
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
39
+ const spec: any = app.getOpenAPI({
40
+ info: {
41
+ title: 'Test API',
42
+ version: '1.0.0'
43
+ }
44
+ });
45
+
46
+ // Check basic structure
47
+ expect(spec.openapi).toBe('3.0.0');
48
+ expect(spec.info.title).toBe('Test API');
49
+
50
+ // Check Path normalization
51
+ expect(spec.paths['/users/{id}']).toBeDefined();
52
+ expect(spec.paths['/health']).toBeDefined();
53
+
54
+ // Check Operation
55
+ const postUser = spec.paths['/users/{id}'].post;
56
+ expect(postUser).toBeDefined();
57
+
58
+ // Check Params
59
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
60
+ const pathParam = postUser.parameters.find((p: any) => p.name === 'id');
61
+ expect(pathParam).toBeDefined();
62
+ expect(pathParam.in).toBe('path');
63
+ expect(pathParam.required).toBe(true);
64
+
65
+ // Check Request Body
66
+ const bodySchema = postUser.requestBody.content['application/json'].schema;
67
+ expect(bodySchema.properties.name.type).toBe('string');
68
+ expect(bodySchema.properties.age.minimum).toBe(18);
69
+
70
+ // Check Response
71
+ const responseSchema = postUser.responses['200'].content['application/json'].schema;
72
+ expect(responseSchema.properties.success.type).toBe('boolean');
73
+ });
74
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { QHTTPXPlugin } from '../src/core/types';
4
+
5
+ describe('Plugin System', () => {
6
+ it('should register a simple plugin', async () => {
7
+ const app = new QHTTPX();
8
+ let pluginRun = false;
9
+
10
+ const myPlugin: QHTTPXPlugin = async (scope) => {
11
+ pluginRun = true;
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ scope.get('/ping', (ctx: any) => ctx.json({ pong: true }));
14
+ };
15
+
16
+ await app.register(myPlugin);
17
+
18
+ expect(pluginRun).toBe(true);
19
+
20
+ const { port } = await app.listen(0);
21
+ const res = await fetch(`http://localhost:${port}/ping`);
22
+ expect(res.status).toBe(200);
23
+ expect(await res.json()).toEqual({ pong: true });
24
+ });
25
+
26
+ it('should handle prefixes', async () => {
27
+ const app = new QHTTPX();
28
+
29
+ const apiPlugin: QHTTPXPlugin = async (scope) => {
30
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
31
+ scope.get('/users', (ctx: any) => ctx.json({ users: [] }));
32
+ };
33
+
34
+ // Register with prefix /v1
35
+ await app.register(apiPlugin, { prefix: '/v1' });
36
+
37
+ const { port } = await app.listen(0);
38
+
39
+ // /v1/users should exist
40
+ const res = await fetch(`http://localhost:${port}/v1/users`);
41
+ expect(res.status).toBe(200);
42
+
43
+ // /users should NOT exist
44
+ const res404 = await fetch(`http://localhost:${port}/users`);
45
+ expect(res404.status).toBe(404);
46
+ });
47
+
48
+ it('should handle nested plugins with concatenated prefixes', async () => {
49
+ const app = new QHTTPX();
50
+
51
+ const usersPlugin: QHTTPXPlugin = async (scope) => {
52
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
53
+ scope.get('/list', (ctx: any) => ctx.json({ list: true }));
54
+ };
55
+
56
+ const v1Plugin: QHTTPXPlugin = async (scope) => {
57
+ // Register nested plugin under /users
58
+ await scope.register(usersPlugin, { prefix: '/users' });
59
+ };
60
+
61
+ // Register v1 under /api
62
+ await app.register(v1Plugin, { prefix: '/api' });
63
+
64
+ const { port } = await app.listen(0);
65
+
66
+ // Result should be /api/users/list
67
+ const res = await fetch(`http://localhost:${port}/api/users/list`);
68
+ expect(res.status).toBe(200);
69
+ expect(await res.json()).toEqual({ list: true });
70
+ });
71
+
72
+ it('should pass options to plugins', async () => {
73
+ const app = new QHTTPX();
74
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
75
+ let receivedOpts: any;
76
+
77
+ const configPlugin: QHTTPXPlugin = async (scope, opts) => {
78
+ receivedOpts = opts;
79
+ };
80
+
81
+ await app.register(configPlugin, { prefix: '/a', secret: '123' });
82
+
83
+ expect(receivedOpts).toEqual({ prefix: '/a', secret: '123' });
84
+ });
85
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { QHTTPXPlugin, QHTTPXContext } from '../src/core/types';
4
+ import { QHTTPXScope } from '../src/core/scope';
5
+ import { createTestClient } from '../src/testing';
6
+
7
+ describe('Plugin System', () => {
8
+ it('should register a simple plugin', async () => {
9
+ const app = new QHTTPX();
10
+
11
+ const myPlugin: QHTTPXPlugin = (s) => {
12
+ const scope = s as QHTTPXScope;
13
+ scope.get('/plugin', (ctx: QHTTPXContext) => {
14
+ ctx.json({ message: 'Hello from Plugin' });
15
+ });
16
+ };
17
+
18
+ await app.register(myPlugin);
19
+
20
+ const client = createTestClient(app);
21
+ const res = await client.get('/plugin');
22
+
23
+ expect(res.status).toBe(200);
24
+ expect(await res.json()).toEqual({ message: 'Hello from Plugin' });
25
+ await client.stop();
26
+ });
27
+
28
+ it('should support scoped prefixes', async () => {
29
+ const app = new QHTTPX();
30
+
31
+ const v1Plugin: QHTTPXPlugin = (s) => {
32
+ const scope = s as QHTTPXScope;
33
+ scope.get('/users', (ctx: QHTTPXContext) => {
34
+ ctx.json({ version: 'v1' });
35
+ });
36
+ };
37
+
38
+ await app.register(v1Plugin, { prefix: '/v1' });
39
+
40
+ const client = createTestClient(app);
41
+ const res = await client.get('/v1/users');
42
+
43
+ expect(res.status).toBe(200);
44
+ expect(await res.json()).toEqual({ version: 'v1' });
45
+ await client.stop();
46
+ });
47
+
48
+ it('should support nested plugins', async () => {
49
+ const app = new QHTTPX();
50
+
51
+ const usersPlugin: QHTTPXPlugin = (s) => {
52
+ const scope = s as QHTTPXScope;
53
+ scope.get('/list', (ctx: QHTTPXContext) => {
54
+ ctx.json({ users: [] });
55
+ });
56
+ };
57
+
58
+ const apiPlugin: QHTTPXPlugin = async (s) => {
59
+ const scope = s as QHTTPXScope;
60
+ await scope.register(usersPlugin, { prefix: '/users' });
61
+ };
62
+
63
+ await app.register(apiPlugin, { prefix: '/api' });
64
+
65
+ const client = createTestClient(app);
66
+ const res = await client.get('/api/users/list');
67
+
68
+ expect(res.status).toBe(200);
69
+ expect(await res.json()).toEqual({ users: [] });
70
+ await client.stop();
71
+ });
72
+
73
+ it('should support plugin options', async () => {
74
+ const app = new QHTTPX();
75
+
76
+ type MyOptions = { secret: string };
77
+ const authPlugin: QHTTPXPlugin<MyOptions> = (s, options) => {
78
+ const scope = s as QHTTPXScope;
79
+ scope.get('/secret', (ctx: QHTTPXContext) => {
80
+ ctx.json({ secret: options.secret });
81
+ });
82
+ };
83
+
84
+ await app.register(authPlugin, { secret: 'super-secret' });
85
+
86
+ const client = createTestClient(app);
87
+ const res = await client.get('/secret');
88
+
89
+ expect(res.status).toBe(200);
90
+ expect(await res.json()).toEqual({ secret: 'super-secret' });
91
+ await client.stop();
92
+ });
93
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { rateLimit } from '../src/middleware/rate-limit';
4
+
5
+ describe('Aegis Rate Limiter', () => {
6
+ let app: QHTTPX;
7
+ let port: number;
8
+
9
+ beforeEach(async () => {
10
+ app = new QHTTPX();
11
+ });
12
+
13
+ afterEach(() => {
14
+ // Cleanup if needed
15
+ });
16
+
17
+ it('should block requests exceeding the limit', async () => {
18
+ app.use(rateLimit({
19
+ windowMs: 1000,
20
+ max: 2, // Allow 2 requests
21
+ message: { error: 'Blocked' }
22
+ }));
23
+
24
+ app.get('/', (ctx) => ctx.json({ ok: true }));
25
+
26
+ const { port: p } = await app.listen(0);
27
+ port = p;
28
+ const url = `http://localhost:${port}`;
29
+
30
+ // 1st request (OK)
31
+ const r1 = await fetch(url);
32
+ expect(r1.status).toBe(200);
33
+ expect(r1.headers.get('x-ratelimit-remaining')).toBe('1');
34
+
35
+ // 2nd request (OK)
36
+ const r2 = await fetch(url);
37
+ expect(r2.status).toBe(200);
38
+ expect(r2.headers.get('x-ratelimit-remaining')).toBe('0');
39
+
40
+ // 3rd request (Blocked)
41
+ const r3 = await fetch(url);
42
+ expect(r3.status).toBe(429);
43
+ expect(await r3.json()).toEqual({ error: 'Blocked' });
44
+ expect(r3.headers.get('retry-after')).toBeDefined();
45
+ });
46
+
47
+ it('should reset after window expires', async () => {
48
+ app.use(rateLimit({
49
+ windowMs: 100, // Short window
50
+ max: 1
51
+ }));
52
+
53
+ app.get('/', (ctx) => ctx.json({ ok: true }));
54
+
55
+ const { port: p } = await app.listen(0);
56
+ const url = `http://localhost:${p}`;
57
+
58
+ // 1st (OK)
59
+ await fetch(url);
60
+
61
+ // 2nd (Blocked)
62
+ const r2 = await fetch(url);
63
+ expect(r2.status).toBe(429);
64
+
65
+ // Wait for window
66
+ await new Promise(r => setTimeout(r, 150));
67
+
68
+ // 3rd (OK)
69
+ const r3 = await fetch(url);
70
+ expect(r3.status).toBe(200);
71
+ });
72
+
73
+ it('should support custom key generators (e.g. per API key)', async () => {
74
+ app.use(rateLimit({
75
+ windowMs: 1000,
76
+ max: 1,
77
+ keyGenerator: (ctx) => ctx.req.headers['x-api-key'] as string || 'anon'
78
+ }));
79
+
80
+ app.get('/', (ctx) => ctx.json({ ok: true }));
81
+
82
+ const { port: p } = await app.listen(0);
83
+ const url = `http://localhost:${p}`;
84
+
85
+ // User A (OK)
86
+ const r1 = await fetch(url, { headers: { 'x-api-key': 'userA' } });
87
+ expect(r1.status).toBe(200);
88
+
89
+ // User B (OK - different key)
90
+ const r2 = await fetch(url, { headers: { 'x-api-key': 'userB' } });
91
+ expect(r2.status).toBe(200);
92
+
93
+ // User A Again (Blocked)
94
+ const r3 = await fetch(url, { headers: { 'x-api-key': 'userA' } });
95
+ expect(r3.status).toBe(429);
96
+ });
97
+ });
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ calculateWorkerCount,
4
+ isResourceOverloaded,
5
+ } from '../src/core/resources';
6
+ import { QHTTPX } from '../src/index';
7
+
8
+ describe('Resource helpers', () => {
9
+ it('calculates worker count for fixed numbers', () => {
10
+ expect(calculateWorkerCount(1)).toBe(1);
11
+ expect(calculateWorkerCount(4)).toBe(4);
12
+ expect(calculateWorkerCount(0)).toBe(1);
13
+ expect(calculateWorkerCount(-5)).toBe(1);
14
+ });
15
+
16
+ it('detects memory overload based on thresholds', () => {
17
+ expect(
18
+ isResourceOverloaded(
19
+ { rssBytes: 2000 },
20
+ {
21
+ maxRssBytes: 1000,
22
+ },
23
+ ),
24
+ ).toBe(true);
25
+
26
+ expect(
27
+ isResourceOverloaded(
28
+ { rssBytes: 500 },
29
+ {
30
+ maxRssBytes: 1000,
31
+ },
32
+ ),
33
+ ).toBe(false);
34
+ });
35
+ });
36
+
37
+ describe('QHTTPX resource-aware behavior', () => {
38
+ it('returns 503 when memory usage exceeds maxMemoryBytes', async () => {
39
+ const originalMemoryUsage = process.memoryUsage;
40
+ const fakeMemoryUsage = vi.fn(() => ({
41
+ ...originalMemoryUsage(),
42
+ rss: 10_000_000_000,
43
+ }));
44
+
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ (process as any).memoryUsage = fakeMemoryUsage;
47
+
48
+ const app = new QHTTPX({
49
+ maxMemoryBytes: 1_000_000,
50
+ });
51
+ const { port } = await app.listen(0, '127.0.0.1');
52
+
53
+ app.get('/', (ctx) => {
54
+ ctx.send('ok');
55
+ });
56
+
57
+ const response = await fetch(`http://127.0.0.1:${port}/`);
58
+ expect(response.status).toBe(503);
59
+
60
+ await app.close();
61
+ (process as unknown as { memoryUsage: typeof originalMemoryUsage }).memoryUsage =
62
+ originalMemoryUsage;
63
+ });
64
+ });