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,78 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { QHTTPX, createHttpApp } from '../src/index';
3
+ import { createApiPreset, createStaticAppPreset } from '../src/middleware/presets';
4
+
5
+ describe('Developer Experience Features', () => {
6
+ describe('Routing Ergonomics', () => {
7
+ it('supports chainable route() builder', async () => {
8
+ const app = new QHTTPX();
9
+
10
+ app.route('/users/:id')
11
+ .get((ctx) => ctx.json({ method: 'GET', id: ctx.params.id }))
12
+ .post((ctx) => ctx.json({ method: 'POST', id: ctx.params.id }))
13
+ .put((ctx) => ctx.json({ method: 'PUT', id: ctx.params.id }))
14
+ .delete((ctx) => ctx.json({ method: 'DELETE', id: ctx.params.id }));
15
+
16
+ const { port } = await app.listen(0, '127.0.0.1');
17
+ const baseUrl = `http://127.0.0.1:${port}`;
18
+
19
+ // Test GET
20
+ const resGet = await fetch(`${baseUrl}/users/123`);
21
+ expect(await resGet.json()).toEqual({ method: 'GET', id: '123' });
22
+
23
+ // Test POST
24
+ const resPost = await fetch(`${baseUrl}/users/123`, { method: 'POST' });
25
+ expect(await resPost.json()).toEqual({ method: 'POST', id: '123' });
26
+
27
+ // Test PUT
28
+ const resPut = await fetch(`${baseUrl}/users/123`, { method: 'PUT' });
29
+ expect(await resPut.json()).toEqual({ method: 'PUT', id: '123' });
30
+
31
+ // Test DELETE
32
+ const resDelete = await fetch(`${baseUrl}/users/123`, { method: 'DELETE' });
33
+ expect(await resDelete.json()).toEqual({ method: 'DELETE', id: '123' });
34
+
35
+ await app.close();
36
+ });
37
+ });
38
+
39
+ describe('Simple App Helper', () => {
40
+ it('supports createHttpApp basic usage', async () => {
41
+ const app = createHttpApp();
42
+
43
+ app.get('/', (ctx) => {
44
+ ctx.send('Hello World!');
45
+ });
46
+
47
+ const { port } = await app.listen(0, '127.0.0.1');
48
+ const response = await fetch(`http://127.0.0.1:${port}/`);
49
+
50
+ expect(response.status).toBe(200);
51
+ expect(await response.text()).toBe('Hello World!');
52
+
53
+ await app.close();
54
+ });
55
+ });
56
+
57
+ describe('Presets', () => {
58
+ it('createApiPreset returns middlewares', () => {
59
+ const middlewares = createApiPreset();
60
+ // Should have CORS (1) + Security Headers (1) + Logger (1) = 3
61
+ expect(middlewares.length).toBe(3);
62
+ });
63
+
64
+ it('createApiPreset allows disabling logger', () => {
65
+ const middlewares = createApiPreset({ logging: false });
66
+ // CORS + Security Headers = 2
67
+ expect(middlewares.length).toBe(2);
68
+ });
69
+
70
+ it('createStaticAppPreset returns middlewares with static', () => {
71
+ const middlewares = createStaticAppPreset({
72
+ static: { root: './public' },
73
+ });
74
+ // CORS + Security Headers + Logger + Static = 4
75
+ expect(middlewares.length).toBe(4);
76
+ });
77
+ });
78
+ });
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { QHTTPXMiddleware } from '../src/core/types';
4
+
5
+ // Mock External Libraries
6
+ const mockJose = {
7
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
+ jwtVerify: async (token: string, _secret: unknown) => {
9
+ if (token === 'valid_token') return { payload: { sub: 'user_123', role: 'admin' } };
10
+ throw new Error('Invalid Token');
11
+ }
12
+ };
13
+
14
+ const mockRedis = {
15
+ get: async (key: string) => {
16
+ if (key === 'cache:page:home') return '<html>Cached Home</html>';
17
+ return null;
18
+ },
19
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
20
+ set: async (key: string, val: string) => 'OK'
21
+ };
22
+
23
+ describe('SaaS Ecosystem Compatibility', () => {
24
+
25
+ it('should allow integration of 3rd party JWT libraries (e.g. jose)', async () => {
26
+ const app = new QHTTPX();
27
+ const secret = 'super-secret';
28
+ // const token = await new SignJWT({ 'urn:example:claim': true })
29
+ // .setProtectedHeader({ alg: 'HS256' })
30
+ // .setIssuedAt()
31
+ // .setExpirationTime('2h')
32
+ // .sign(new TextEncoder().encode(secret));
33
+
34
+ // 1. Create a custom Authentication Middleware using "jose"
35
+ const authMiddleware: QHTTPXMiddleware = async (ctx, next) => {
36
+ const authHeader = ctx.req.headers['authorization'];
37
+ if (!authHeader) {
38
+ ctx.res.statusCode = 401;
39
+ ctx.res.end('Unauthorized');
40
+ return;
41
+ }
42
+
43
+ try {
44
+ const token = authHeader.split(' ')[1];
45
+ const { payload } = await mockJose.jwtVerify(token, secret);
46
+
47
+ // Store user in ctx.state (Standard Pattern)
48
+ ctx.state.user = payload;
49
+ await next();
50
+ } catch {
51
+ ctx.res.statusCode = 403;
52
+ ctx.res.end('Forbidden');
53
+ }
54
+ };
55
+
56
+ app.use(authMiddleware);
57
+
58
+ // Protected Route
59
+ app.get('/dashboard', async (ctx) => {
60
+ // Access the user injected by middleware
61
+ const user = ctx.state.user;
62
+ ctx.json({ message: `Welcome ${user.sub}`, role: user.role });
63
+ });
64
+
65
+ const { port } = await app.listen(0);
66
+ const url = `http://localhost:${port}`;
67
+
68
+ // Test Valid Token
69
+ const res1 = await fetch(`${url}/dashboard`, { headers: { Authorization: 'Bearer valid_token' } });
70
+ expect(res1.status).toBe(200);
71
+ const data1 = await res1.json();
72
+ expect(data1).toEqual({ message: 'Welcome user_123', role: 'admin' });
73
+
74
+ // Test Invalid Token
75
+ const res2 = await fetch(`${url}/dashboard`, { headers: { Authorization: 'Bearer bad_token' } });
76
+ expect(res2.status).toBe(403);
77
+
78
+ await app.close();
79
+ });
80
+
81
+ it('should allow integration of 3rd party Caching (e.g. Redis)', async () => {
82
+ const app = new QHTTPX();
83
+
84
+ // 2. Create a Caching Middleware using "redis"
85
+ const cacheMiddleware: QHTTPXMiddleware = async (ctx, next) => {
86
+ const key = `cache:page:${ctx.url.pathname.replace('/', '')}`;
87
+ const cached = await mockRedis.get(key);
88
+
89
+ if (cached) {
90
+ ctx.res.setHeader('X-Cache', 'HIT');
91
+ ctx.res.end(cached);
92
+ return;
93
+ }
94
+
95
+ ctx.res.setHeader('X-Cache', 'MISS');
96
+ await next();
97
+ };
98
+
99
+ app.use(cacheMiddleware);
100
+
101
+ app.get('/home', async (ctx) => {
102
+ ctx.html('<html>Fresh Home</html>');
103
+ });
104
+
105
+ const { port } = await app.listen(0);
106
+ const url = `http://localhost:${port}`;
107
+
108
+ // Test Cache Hit (Mocked)
109
+ const res1 = await fetch(`${url}/home`);
110
+ expect(res1.status).toBe(200);
111
+ expect(res1.headers.get('x-cache')).toBe('HIT');
112
+ const text1 = await res1.text();
113
+ expect(text1).toBe('<html>Cached Home</html>');
114
+
115
+ // Test Cache Miss (Mocked for other route)
116
+ // Note: In a real app we'd need to intercept res.end to write to redis,
117
+ // but for this capability check, reading is enough proof.
118
+
119
+ await app.close();
120
+ });
121
+
122
+ it('should allow integration of ORM/DB libraries', async () => {
123
+ const app = new QHTTPX();
124
+
125
+ // Mock TypeORM/Prisma usage
126
+ const db = {
127
+ users: {
128
+ findMany: async () => [{ id: 1, name: 'Alice' }],
129
+ },
130
+ };
131
+
132
+ // Middleware to attach DB to context
133
+ app.use(async (ctx, next) => {
134
+ // We can attach to ctx.state or extend ctx type if we want
135
+ ctx.state.db = db;
136
+ await next();
137
+ });
138
+
139
+ app.get('/users', async (ctx) => {
140
+ try {
141
+ const users = await ctx.state.db.users.findMany();
142
+ ctx.res.end(JSON.stringify(users));
143
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
144
+ } catch (err) {
145
+ ctx.res.statusCode = 500;
146
+ ctx.res.end('DB Error');
147
+ }
148
+ });
149
+
150
+ const { port } = await app.listen(0);
151
+ const url = `http://localhost:${port}`;
152
+ const res = await fetch(`${url}/users`);
153
+ expect(res.status).toBe(200);
154
+ await app.close();
155
+ });
156
+ });
@@ -0,0 +1,51 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { QHTTPX } from '../src/index';
3
+ import { Logger } from '../src/core/logger';
4
+ import { hc } from '../src/client';
5
+
6
+ describe('Logger', () => {
7
+ it('should initialize with pino', () => {
8
+ const logger = new Logger({ level: 'info' });
9
+ expect(logger).toBeDefined();
10
+ // Check if methods exist (can't easily mock internal pino without spy)
11
+ expect(typeof logger.info).toBe('function');
12
+ expect(typeof logger.error).toBe('function');
13
+ });
14
+
15
+ it('should be integrated into QHTTPX', () => {
16
+ const app = new QHTTPX();
17
+ expect(app.logger).toBeDefined();
18
+ expect(app.logger instanceof Logger).toBe(true);
19
+ });
20
+ });
21
+
22
+ describe('RPC Client', () => {
23
+ it('should construct correct URLs and methods', async () => {
24
+ // We mock fetch globally
25
+ const originalFetch = global.fetch;
26
+ global.fetch = async (url: RequestInfo | URL, options?: RequestInit) => {
27
+ return new Response(JSON.stringify({ url, method: options?.method }), {
28
+ status: 200,
29
+ headers: { 'Content-Type': 'application/json' },
30
+ });
31
+ };
32
+
33
+ try {
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ const client = hc<any>('http://localhost:3000');
36
+
37
+ const res1 = await client.api.users[':id'].$get({ param: { id: '123' } });
38
+ const data1 = await res1.json();
39
+ expect(data1.url).toBe('http://localhost:3000/api/users/123');
40
+ expect(data1.method).toBe('GET');
41
+
42
+ const res2 = await client.auth.login.$post({ json: { username: 'test' } });
43
+ const data2 = await res2.json();
44
+ expect(data2.url).toBe('http://localhost:3000/auth/login');
45
+ expect(data2.method).toBe('POST');
46
+
47
+ } finally {
48
+ global.fetch = originalFetch;
49
+ }
50
+ });
51
+ });
@@ -0,0 +1,121 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+
4
+ describe('Request Fusion Engine', () => {
5
+ let app: QHTTPX;
6
+
7
+ afterEach(async () => {
8
+ if (app) await app.close();
9
+ });
10
+
11
+ it('should coalesce simultaneous requests to the same endpoint', async () => {
12
+ app = new QHTTPX({ enableRequestFusion: true });
13
+
14
+ let executionCount = 0;
15
+
16
+ // Simulate a slow handler
17
+ app.get('/slow', async (ctx) => {
18
+ executionCount++;
19
+ await new Promise(resolve => setTimeout(resolve, 300));
20
+ ctx.json({ count: executionCount });
21
+ });
22
+
23
+ const { port } = await app.listen(0);
24
+ const url = `http://localhost:${port}/slow`;
25
+
26
+ // Fire 3 requests simultaneously
27
+ const p1 = fetch(url).then(r => r.json());
28
+ const p2 = fetch(url).then(r => r.json());
29
+ const p3 = fetch(url).then(r => r.json());
30
+
31
+ const results = await Promise.all([p1, p2, p3]);
32
+
33
+ // All should get the same result
34
+ expect(results[0]).toEqual({ count: 1 });
35
+ expect(results[1]).toEqual({ count: 1 });
36
+ expect(results[2]).toEqual({ count: 1 });
37
+
38
+ // Handler should have run only once
39
+ expect(executionCount).toBe(1);
40
+ });
41
+
42
+ it('should NOT coalesce requests with different query params', async () => {
43
+ app = new QHTTPX({ enableRequestFusion: true });
44
+
45
+ let executionCount = 0;
46
+
47
+ app.get('/echo', async (ctx) => {
48
+ executionCount++;
49
+ await new Promise(resolve => setTimeout(resolve, 20));
50
+ ctx.json({ q: ctx.query.q, count: executionCount });
51
+ });
52
+
53
+ const { port } = await app.listen(0);
54
+
55
+ const p1 = fetch(`http://localhost:${port}/echo?q=a`).then(r => r.json());
56
+ const p2 = fetch(`http://localhost:${port}/echo?q=b`).then(r => r.json());
57
+
58
+ const results = await Promise.all([p1, p2]);
59
+
60
+ expect(results[0].q).toBe('a');
61
+ expect(results[1].q).toBe('b');
62
+
63
+ // Should run twice because keys are different
64
+ expect(executionCount).toBe(2);
65
+ });
66
+
67
+ it('should use cache window to coalesce burst traffic', async () => {
68
+ // 50ms window
69
+ app = new QHTTPX({
70
+ enableRequestFusion: { windowMs: 50 }
71
+ });
72
+
73
+ let executionCount = 0;
74
+
75
+ app.get('/burst', async (ctx) => {
76
+ executionCount++;
77
+ // Fast handler
78
+ ctx.json({ count: executionCount });
79
+ });
80
+
81
+ const { port } = await app.listen(0);
82
+ const url = `http://localhost:${port}/burst`;
83
+
84
+ // Request 1
85
+ const r1 = await fetch(url).then(r => r.json());
86
+ expect(r1.count).toBe(1);
87
+
88
+ // Request 2 (immediate, within window)
89
+ const r2 = await fetch(url).then(r => r.json());
90
+ expect(r2.count).toBe(1); // Should be cached
91
+
92
+ // Request 3 (wait 100ms, outside window)
93
+ await new Promise(resolve => setTimeout(resolve, 100));
94
+ const r3 = await fetch(url).then(r => r.json());
95
+ expect(r3.count).toBe(2); // New execution
96
+ });
97
+
98
+ it('should respect vary headers (Authorization)', async () => {
99
+ app = new QHTTPX({ enableRequestFusion: true });
100
+
101
+ let executionCount = 0;
102
+
103
+ app.get('/auth', async (ctx) => {
104
+ executionCount++;
105
+ await new Promise(resolve => setTimeout(resolve, 20));
106
+ ctx.json({ user: ctx.req.headers['authorization'], count: executionCount });
107
+ });
108
+
109
+ const { port } = await app.listen(0);
110
+ const url = `http://localhost:${port}/auth`;
111
+
112
+ const p1 = fetch(url, { headers: { 'Authorization': 'UserA' } }).then(r => r.json());
113
+ const p2 = fetch(url, { headers: { 'Authorization': 'UserB' } }).then(r => r.json());
114
+
115
+ const results = await Promise.all([p1, p2]);
116
+
117
+ expect(results[0].user).toBe('UserA');
118
+ expect(results[1].user).toBe('UserB');
119
+ expect(executionCount).toBe(2);
120
+ });
121
+ });
@@ -0,0 +1,161 @@
1
+ import { afterEach, describe, expect, it } from 'vitest';
2
+ import { QHTTPX } from '../src/index';
3
+
4
+ describe('QHTTPX minimal HTTP runtime', () => {
5
+ let app: QHTTPX | undefined;
6
+
7
+ afterEach(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+
14
+ it('handles GET / and returns JSON', async () => {
15
+ app = new QHTTPX();
16
+
17
+ app.get('/', (ctx) => {
18
+ ctx.json({ message: 'Hello from QHTTPX' });
19
+ });
20
+
21
+ const { port } = await app.listen(0, '127.0.0.1');
22
+
23
+ const response = await fetch(`http://127.0.0.1:${port}/`);
24
+ expect(response.status).toBe(200);
25
+ const json = (await response.json()) as { message: string };
26
+ expect(json.message).toBe('Hello from QHTTPX');
27
+ });
28
+
29
+ it('returns 404 for unknown route', async () => {
30
+ app = new QHTTPX();
31
+
32
+ const { port } = await app.listen(0, '127.0.0.1');
33
+
34
+ const response = await fetch(`http://127.0.0.1:${port}/unknown`);
35
+ expect(response.status).toBe(404);
36
+ });
37
+
38
+ it('supports params and query parsing', async () => {
39
+ app = new QHTTPX();
40
+
41
+ app.get('/users/:id', (ctx) => {
42
+ ctx.json({
43
+ id: ctx.params.id,
44
+ q: ctx.query.q,
45
+ });
46
+ });
47
+
48
+ const { port } = await app.listen(0, '127.0.0.1');
49
+
50
+ const response = await fetch(`http://127.0.0.1:${port}/users/123?q=test`);
51
+ expect(response.status).toBe(200);
52
+ const json = (await response.json()) as { id: string; q: string };
53
+ expect(json.id).toBe('123');
54
+ expect(json.q).toBe('test');
55
+ });
56
+
57
+ it('parses JSON body for POST', async () => {
58
+ app = new QHTTPX();
59
+
60
+ app.post('/echo', (ctx) => {
61
+ ctx.json({ body: ctx.body });
62
+ });
63
+
64
+ const { port } = await app.listen(0, '127.0.0.1');
65
+
66
+ const response = await fetch(`http://127.0.0.1:${port}/echo`, {
67
+ method: 'POST',
68
+ headers: {
69
+ 'content-type': 'application/json',
70
+ },
71
+ body: JSON.stringify({ hello: 'world' }),
72
+ });
73
+
74
+ expect(response.status).toBe(200);
75
+ const json = (await response.json()) as { body: { hello: string } };
76
+ expect(json.body.hello).toBe('world');
77
+ });
78
+
79
+ it('returns 400 for invalid JSON body', async () => {
80
+ app = new QHTTPX();
81
+
82
+ const { port } = await app.listen(0, '127.0.0.1');
83
+
84
+ app.post('/echo', (ctx) => {
85
+ ctx.json({ body: ctx.body });
86
+ });
87
+
88
+ const response = await fetch(`http://127.0.0.1:${port}/echo`, {
89
+ method: 'POST',
90
+ headers: {
91
+ 'content-type': 'application/json',
92
+ },
93
+ body: '{ invalid',
94
+ });
95
+
96
+ expect(response.status).toBe(400);
97
+ const text = await response.text();
98
+ expect(text).toBe('Invalid JSON');
99
+ });
100
+
101
+ it('enforces maxBodyBytes with 413 status', async () => {
102
+ app = new QHTTPX({ maxBodyBytes: 8 });
103
+
104
+ app.post('/echo', (ctx) => {
105
+ ctx.json({ body: ctx.body });
106
+ });
107
+
108
+ const { port } = await app.listen(0, '127.0.0.1');
109
+
110
+ const response = await fetch(`http://127.0.0.1:${port}/echo`, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'content-type': 'application/json',
114
+ },
115
+ body: JSON.stringify({ hello: 'world' }),
116
+ });
117
+
118
+ expect(response.status).toBe(413);
119
+ const text = await response.text();
120
+ expect(text).toBe('Payload Too Large');
121
+ });
122
+
123
+ it('enforces maxConcurrency with 503 overload', async () => {
124
+ app = new QHTTPX({ maxConcurrency: 1 });
125
+
126
+ app.get('/slow', async (ctx) => {
127
+ await new Promise<void>((resolve) => {
128
+ setTimeout(resolve, 100);
129
+ });
130
+ ctx.json({ ok: true });
131
+ });
132
+
133
+ const { port } = await app.listen(0, '127.0.0.1');
134
+
135
+ const url = `http://127.0.0.1:${port}/slow`;
136
+
137
+ const first = fetch(url);
138
+ const second = fetch(url);
139
+
140
+ const [firstRes, secondRes] = await Promise.all([first, second]);
141
+
142
+ const statuses = [firstRes.status, secondRes.status].sort();
143
+ expect(statuses).toEqual([200, 503]);
144
+ });
145
+
146
+ it('applies request timeout with 504 status', async () => {
147
+ app = new QHTTPX({ requestTimeoutMs: 20 });
148
+
149
+ app.get('/timeout', async (ctx) => {
150
+ await new Promise<void>((resolve) => {
151
+ setTimeout(resolve, 100);
152
+ });
153
+ ctx.json({ ok: true });
154
+ });
155
+
156
+ const { port } = await app.listen(0, '127.0.0.1');
157
+
158
+ const response = await fetch(`http://127.0.0.1:${port}/timeout`);
159
+ expect(response.status).toBe(504);
160
+ });
161
+ });
@@ -0,0 +1,48 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { QHTTPX, createLoggerMiddleware } from '../src';
3
+
4
+ describe('Logger middleware', () => {
5
+ let app: QHTTPX | undefined;
6
+
7
+ afterEach(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+
14
+ it('logs basic request information to sink', async () => {
15
+ const sink = vi.fn();
16
+
17
+ app = new QHTTPX();
18
+ app.use(
19
+ createLoggerMiddleware({
20
+ sink,
21
+ }),
22
+ );
23
+
24
+ app.get('/hello', (ctx) => {
25
+ ctx.send('ok');
26
+ });
27
+
28
+ const { port } = await app.listen(0, '127.0.0.1');
29
+
30
+ const response = await fetch(`http://127.0.0.1:${port}/hello`);
31
+
32
+ expect(response.status).toBe(200);
33
+ expect(await response.text()).toBe('ok');
34
+
35
+ expect(sink).toHaveBeenCalledTimes(1);
36
+ const entry = sink.mock.calls[0][0] as {
37
+ method: string;
38
+ path: string;
39
+ status: number;
40
+ durationMs: number;
41
+ };
42
+ expect(entry.method).toBe('GET');
43
+ expect(entry.path).toBe('/hello');
44
+ expect(entry.status).toBe(200);
45
+ expect(entry.durationMs).toBeGreaterThanOrEqual(0);
46
+ });
47
+ });
48
+