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 { afterEach, describe, expect, it } from 'vitest';
2
+ import { QHTTPX } from '../src/index';
3
+
4
+ type TaskMetricsJson = {
5
+ tasks: {
6
+ registeredTasks: number;
7
+ totalEnqueued: number;
8
+ totalCompleted: number;
9
+ totalFailed: number;
10
+ totalOverloaded: number;
11
+ totalRetried?: number;
12
+ };
13
+ };
14
+
15
+ describe('Task Metrics', () => {
16
+ let app: QHTTPX | undefined;
17
+
18
+ afterEach(async () => {
19
+ if (app) {
20
+ await app.close();
21
+ app = undefined;
22
+ }
23
+ });
24
+
25
+ it('exposes task metrics in /__qhttpx/metrics', async () => {
26
+ app = new QHTTPX();
27
+
28
+ // Register a simple task
29
+ app.task('simple-task', async () => {
30
+ // do nothing
31
+ });
32
+
33
+ const { port } = await app.listen(0, '127.0.0.1');
34
+
35
+ // Enqueue the task a few times
36
+ await app.enqueue('simple-task', {});
37
+ await app.enqueue('simple-task', {});
38
+
39
+ // Allow some time for async tasks to complete
40
+ await new Promise((resolve) => setTimeout(resolve, 50));
41
+
42
+ const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
43
+ expect(response.status).toBe(200);
44
+
45
+ const json = (await response.json()) as TaskMetricsJson;
46
+
47
+ expect(json.tasks).toBeDefined();
48
+ expect(json.tasks.registeredTasks).toBe(1);
49
+ expect(json.tasks.totalEnqueued).toBe(2);
50
+ expect(json.tasks.totalCompleted).toBe(2);
51
+ expect(json.tasks.totalFailed).toBe(0);
52
+ expect(json.tasks.totalOverloaded).toBe(0);
53
+ });
54
+
55
+ it('counts failed tasks', async () => {
56
+ app = new QHTTPX();
57
+
58
+ app.task('failing-task', async () => {
59
+ throw new Error('Task failed');
60
+ }, { maxRetries: 1 }); // 1 retry
61
+
62
+ const { port } = await app.listen(0, '127.0.0.1');
63
+
64
+ try {
65
+ await app.enqueue('failing-task', {});
66
+ } catch {
67
+ }
68
+
69
+ // Allow time for retry and failure
70
+ await new Promise((resolve) => setTimeout(resolve, 50));
71
+
72
+ const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
73
+ const json = (await response.json()) as TaskMetricsJson;
74
+
75
+ expect(json.tasks.totalFailed).toBe(1);
76
+ expect(json.tasks.totalRetried).toBe(1); // 1 retry attempt
77
+ });
78
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { QHTTPX } from '../src/index';
3
+
4
+ describe('Task engine', () => {
5
+ it('registers and executes a task', async () => {
6
+ const app = new QHTTPX();
7
+
8
+ const seen: unknown[] = [];
9
+
10
+ app.task('echo', async (payload) => {
11
+ seen.push(payload);
12
+ });
13
+
14
+ await app.enqueue('echo', { hello: 'world' });
15
+
16
+ expect(seen).toHaveLength(1);
17
+ expect(seen[0]).toEqual({ hello: 'world' });
18
+ });
19
+
20
+ it('retries failing tasks according to maxRetries', async () => {
21
+ const app = new QHTTPX();
22
+
23
+ const spy = vi.fn();
24
+ let attempts = 0;
25
+
26
+ app.task(
27
+ 'sometimes-fails',
28
+ () => {
29
+ spy();
30
+ attempts += 1;
31
+ if (attempts < 3) {
32
+ throw new Error('fail');
33
+ }
34
+ },
35
+ { maxRetries: 5 },
36
+ );
37
+
38
+ await app.enqueue('sometimes-fails', null);
39
+
40
+ expect(spy).toHaveBeenCalledTimes(3);
41
+ });
42
+
43
+ it('propagates errors after exceeding retries', async () => {
44
+ const app = new QHTTPX();
45
+
46
+ app.task(
47
+ 'always-fails',
48
+ () => {
49
+ throw new Error('boom');
50
+ },
51
+ { maxRetries: 1 },
52
+ );
53
+
54
+ await expect(app.enqueue('always-fails', null)).rejects.toThrow('boom');
55
+ });
56
+
57
+ it('integrates with HTTP route that enqueues a job', async () => {
58
+ const app = new QHTTPX();
59
+ const seen: string[] = [];
60
+
61
+ app.task('job', async (payload) => {
62
+ seen.push(payload as string);
63
+ });
64
+
65
+ app.post('/enqueue', async (ctx) => {
66
+ const body = ctx.body as { value: string };
67
+ await app.enqueue('job', body.value);
68
+ ctx.json({ ok: true });
69
+ });
70
+
71
+ const { port } = await app.listen(0, '127.0.0.1');
72
+
73
+ const response = await fetch(`http://127.0.0.1:${port}/enqueue`, {
74
+ method: 'POST',
75
+ headers: {
76
+ 'content-type': 'application/json',
77
+ },
78
+ body: JSON.stringify({ value: 'hello' }),
79
+ });
80
+
81
+ expect(response.status).toBe(200);
82
+ const json = (await response.json()) as { ok: boolean };
83
+ expect(json.ok).toBe(true);
84
+
85
+ expect(seen).toEqual(['hello']);
86
+
87
+ await app.close();
88
+ });
89
+ });
90
+
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { QHTTPX, createTestClient } from '../src';
3
+
4
+ describe('Testing utilities', () => {
5
+ it('creates a test client and handles requests', async () => {
6
+ const app = new QHTTPX();
7
+
8
+ app.get('/hello', (ctx) => {
9
+ ctx.json({ message: 'world' });
10
+ });
11
+
12
+ app.post('/echo', (ctx) => {
13
+ ctx.json(ctx.body);
14
+ });
15
+
16
+ const client = createTestClient(app);
17
+
18
+ try {
19
+ const res1 = await client.get('/hello');
20
+ expect(res1.status).toBe(200);
21
+ expect(await res1.json()).toEqual({ message: 'world' });
22
+
23
+ const res2 = await client.post('/echo', { foo: 'bar' });
24
+ expect(res2.status).toBe(200);
25
+ expect(await res2.json()).toEqual({ foo: 'bar' });
26
+ } finally {
27
+ await client.stop();
28
+ }
29
+ });
30
+
31
+ it('handles custom headers and raw body', async () => {
32
+ const app = new QHTTPX();
33
+
34
+ app.post('/raw', (ctx) => {
35
+ ctx.send(ctx.req.headers['x-custom'] as string);
36
+ });
37
+
38
+ const client = createTestClient(app);
39
+
40
+ try {
41
+ const res = await client.post('/raw', 'raw-body', {
42
+ headers: {
43
+ 'x-custom': 'custom-value',
44
+ 'content-type': 'text/plain',
45
+ },
46
+ });
47
+ expect(res.status).toBe(200);
48
+ expect(await res.text()).toBe('custom-value');
49
+ } finally {
50
+ await client.stop();
51
+ }
52
+ });
53
+ });
@@ -0,0 +1,126 @@
1
+ import { describe, it, expect, afterEach } from 'vitest';
2
+ import { QHTTPX } from '../src/core/server';
3
+ import { RouteSchema } from '../src/validation/types';
4
+
5
+ describe('Validation System', () => {
6
+ let app: QHTTPX;
7
+
8
+ afterEach(async () => {
9
+ if (app) await app.close();
10
+ });
11
+
12
+ it('should validate request body and return 400 on failure', async () => {
13
+ app = new QHTTPX();
14
+
15
+ const schema: RouteSchema = {
16
+ body: {
17
+ type: 'object',
18
+ properties: {
19
+ name: { type: 'string', min: 3 },
20
+ age: { type: 'number', min: 18 }
21
+ }
22
+ }
23
+ };
24
+
25
+ app.post('/user', {
26
+ schema,
27
+ handler: (ctx: import('../src/core/types').QHTTPXContext) => {
28
+ ctx.json({ status: 'ok', body: ctx.body });
29
+ }
30
+ });
31
+
32
+ const { port } = await app.listen(0);
33
+
34
+ // Invalid request (age too low)
35
+ const res1 = await fetch(`http://localhost:${port}/user`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ name: 'Alice', age: 10 })
39
+ });
40
+
41
+ expect(res1.status).toBe(400);
42
+ const data1 = await res1.json();
43
+ expect(data1.error).toBe('Validation Error');
44
+ expect(data1.details).toContain('age');
45
+
46
+ // Valid request
47
+ const res2 = await fetch(`http://localhost:${port}/user`, {
48
+ method: 'POST',
49
+ headers: { 'Content-Type': 'application/json' },
50
+ body: JSON.stringify({ name: 'Alice', age: 20 })
51
+ });
52
+
53
+ expect(res2.status).toBe(200);
54
+ const data2 = await res2.json();
55
+ expect(data2.body).toEqual({ name: 'Alice', age: 20 });
56
+ });
57
+
58
+ it('should validate and coerce query parameters', async () => {
59
+ app = new QHTTPX();
60
+
61
+ const schema: RouteSchema = {
62
+ query: {
63
+ type: 'object',
64
+ properties: {
65
+ page: { type: 'number', min: 1 },
66
+ sort: { type: 'string', enum: ['asc', 'desc'] }
67
+ }
68
+ }
69
+ };
70
+
71
+ app.get('/items', {
72
+ schema,
73
+ handler: (ctx: import('../src/core/types').QHTTPXContext) => {
74
+ // ctx.query should have numbers now
75
+ ctx.json({
76
+ page: ctx.query.page,
77
+ pageType: typeof ctx.query.page,
78
+ sort: ctx.query.sort
79
+ });
80
+ }
81
+ });
82
+
83
+ const { port } = await app.listen(0);
84
+
85
+ // Valid query
86
+ const res1 = await fetch(`http://localhost:${port}/items?page=5&sort=desc`);
87
+ expect(res1.status).toBe(200);
88
+ const data1 = await res1.json();
89
+ expect(data1.page).toBe(5);
90
+ expect(data1.pageType).toBe('number'); // Coercion worked
91
+ expect(data1.sort).toBe('desc');
92
+
93
+ // Invalid query
94
+ const res2 = await fetch(`http://localhost:${port}/items?page=0&sort=foo`);
95
+ expect(res2.status).toBe(400);
96
+ });
97
+
98
+ it('should support legacy response schema (no validation)', async () => {
99
+ app = new QHTTPX();
100
+
101
+ // Legacy schema: just a JSON schema at root
102
+ const schema = {
103
+ type: 'object',
104
+ properties: {
105
+ hello: { type: 'string' }
106
+ }
107
+ };
108
+
109
+ app.get('/legacy', {
110
+ schema,
111
+ handler: (ctx: import('../src/core/types').QHTTPXContext) => {
112
+ // If validation ran, it might fail because ctx.body is undefined or not matching
113
+ // But here we check if it IGNORES validation and uses it for serialization
114
+ ctx.json({ hello: 'world', extra: 'hidden' });
115
+ }
116
+ });
117
+
118
+ const { port } = await app.listen(0);
119
+
120
+ const res = await fetch(`http://localhost:${port}/legacy`);
121
+ expect(res.status).toBe(200);
122
+ const data = await res.json();
123
+ expect(data.hello).toBe('world');
124
+ expect(data.extra).toBeUndefined(); // fast-json-stringify filters extra fields
125
+ });
126
+ });
@@ -0,0 +1,132 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import * as net from 'net';
3
+ import WebSocket from 'ws';
4
+ import { QHTTPX } from '../src';
5
+
6
+ describe('WebSocket upgrade', () => {
7
+ it('handles websocket upgrade for registered path', async () => {
8
+ const app = new QHTTPX();
9
+ let upgraded = false;
10
+
11
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
12
+ app.upgrade('/ws', (ws, req) => {
13
+ upgraded = true;
14
+ ws.close();
15
+ });
16
+
17
+ const { port } = await app.listen(0, '127.0.0.1');
18
+
19
+ const ws = new WebSocket(`ws://127.0.0.1:${port}/ws`);
20
+
21
+ await new Promise<void>((resolve, reject) => {
22
+ ws.on('open', () => {
23
+ resolve();
24
+ });
25
+ ws.on('error', (err) => {
26
+ reject(err);
27
+ });
28
+ });
29
+
30
+ expect(upgraded).toBe(true);
31
+ ws.close();
32
+ await app.close();
33
+ });
34
+
35
+ it('destroys socket when no upgrade handler is registered for path', async () => {
36
+ const app = new QHTTPX();
37
+ const { port } = await app.listen(0, '127.0.0.1');
38
+
39
+ const socket = net.createConnection({ port, host: '127.0.0.1' });
40
+
41
+ const done = new Promise<void>((resolve) => {
42
+ let finished = false;
43
+ const finish = () => {
44
+ if (!finished) {
45
+ finished = true;
46
+ resolve();
47
+ }
48
+ };
49
+ socket.on('end', finish);
50
+ socket.on('close', finish);
51
+ socket.on('error', () => {
52
+ finish();
53
+ });
54
+ setTimeout(finish, 500);
55
+ });
56
+
57
+ const requestLines = [
58
+ 'GET /no-ws HTTP/1.1',
59
+ 'Host: 127.0.0.1',
60
+ 'Upgrade: websocket',
61
+ 'Connection: Upgrade',
62
+ '',
63
+ '',
64
+ ];
65
+
66
+ socket.write(requestLines.join('\r\n'));
67
+
68
+ await done;
69
+ await app.close();
70
+ });
71
+
72
+ it('supports rooms and broadcasting', async () => {
73
+ const app = new QHTTPX();
74
+
75
+ app.upgrade('/chat', (ws, req) => {
76
+ const url = new URL(req.url || '', 'http://localhost');
77
+ const room = url.searchParams.get('room');
78
+ if (room) {
79
+ ws.join(room);
80
+ }
81
+ });
82
+
83
+ app.post('/broadcast', (ctx) => {
84
+ const { room, message } = ctx.body as { room: string; message: string };
85
+ app.websocket.to(room).emit(message);
86
+ ctx.json({ success: true });
87
+ });
88
+
89
+ const { port } = await app.listen(0, '127.0.0.1');
90
+ const baseUrl = `ws://127.0.0.1:${port}/chat`;
91
+
92
+ // Client A in room1
93
+ const wsA = new WebSocket(`${baseUrl}?room=room1`);
94
+ const msgsA: string[] = [];
95
+ wsA.on('message', (data) => msgsA.push(data.toString()));
96
+
97
+ // Client B in room1
98
+ const wsB = new WebSocket(`${baseUrl}?room=room1`);
99
+ const msgsB: string[] = [];
100
+ wsB.on('message', (data) => msgsB.push(data.toString()));
101
+
102
+ // Client C in room2
103
+ const wsC = new WebSocket(`${baseUrl}?room=room2`);
104
+ const msgsC: string[] = [];
105
+ wsC.on('message', (data) => msgsC.push(data.toString()));
106
+
107
+ await Promise.all([
108
+ new Promise<void>((resolve) => wsA.on('open', () => resolve())),
109
+ new Promise<void>((resolve) => wsB.on('open', () => resolve())),
110
+ new Promise<void>((resolve) => wsC.on('open', () => resolve())),
111
+ ]);
112
+
113
+ // Broadcast to room1
114
+ // We need to make a POST request.
115
+ // Since we are inside test, we can use fetch or http.request.
116
+ // Or just use app.websocket directly if we can access it?
117
+ // We can access app.websocket directly here since we have app instance.
118
+ app.websocket.to('room1').emit('hello room1');
119
+
120
+ // Wait a bit for messages
121
+ await new Promise((resolve) => setTimeout(resolve, 100));
122
+
123
+ expect(msgsA).toContain('hello room1');
124
+ expect(msgsB).toContain('hello room1');
125
+ expect(msgsC).not.toContain('hello room1');
126
+
127
+ wsA.close();
128
+ wsB.close();
129
+ wsC.close();
130
+ await app.close();
131
+ });
132
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "commonjs",
5
+ "moduleResolution": "node",
6
+ "rootDir": ".",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "skipLibCheck": true,
12
+ "resolveJsonModule": true,
13
+ "types": ["node", "vitest"]
14
+ },
15
+ "include": ["src", "tests", "vitest.config.*"]
16
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['tests/**/*.test.ts'],
6
+ exclude: ['dist/**', 'node_modules/**'],
7
+ },
8
+ });
9
+