qhttpx 1.9.2 → 1.9.3

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 (232) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +79 -17
  3. package/dist/examples/api-server.d.ts +1 -0
  4. package/dist/examples/api-server.js +77 -0
  5. package/dist/examples/basic.d.ts +1 -0
  6. package/dist/examples/basic.js +10 -0
  7. package/dist/examples/compression.d.ts +1 -0
  8. package/dist/examples/compression.js +17 -0
  9. package/dist/examples/cors.d.ts +1 -0
  10. package/dist/examples/cors.js +19 -0
  11. package/dist/examples/errors.d.ts +1 -0
  12. package/dist/examples/errors.js +25 -0
  13. package/dist/examples/file-upload.d.ts +1 -0
  14. package/dist/examples/file-upload.js +24 -0
  15. package/dist/examples/fusion.d.ts +1 -0
  16. package/dist/examples/fusion.js +21 -0
  17. package/dist/examples/rate-limiting.d.ts +1 -0
  18. package/dist/examples/rate-limiting.js +17 -0
  19. package/dist/examples/validation.d.ts +1 -0
  20. package/dist/examples/validation.js +23 -0
  21. package/dist/examples/websockets.d.ts +1 -0
  22. package/dist/examples/websockets.js +20 -0
  23. package/dist/package.json +112 -0
  24. package/dist/src/benchmarks/compare-frameworks.js +119 -0
  25. package/dist/src/benchmarks/compare.d.ts +1 -0
  26. package/dist/src/benchmarks/compare.js +288 -0
  27. package/dist/src/benchmarks/quantam-users.d.ts +1 -0
  28. package/dist/src/benchmarks/quantam-users.js +56 -0
  29. package/dist/src/benchmarks/simple-json.d.ts +1 -0
  30. package/dist/src/benchmarks/simple-json.js +60 -0
  31. package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
  32. package/dist/src/benchmarks/ultra-mode.js +94 -0
  33. package/dist/src/buffer-pool.js +70 -0
  34. package/dist/src/cli/index.d.ts +2 -0
  35. package/dist/src/cli/index.js +222 -0
  36. package/dist/src/client/index.d.ts +17 -0
  37. package/dist/src/client/index.js +72 -0
  38. package/dist/src/config.js +50 -0
  39. package/dist/src/cookies.js +59 -0
  40. package/dist/src/core/batch.d.ts +24 -0
  41. package/dist/src/core/batch.js +97 -0
  42. package/dist/src/core/body-parser.d.ts +15 -0
  43. package/dist/src/core/body-parser.js +121 -0
  44. package/dist/src/core/buffer-pool.d.ts +41 -0
  45. package/dist/src/core/buffer-pool.js +70 -0
  46. package/dist/src/core/config.d.ts +7 -0
  47. package/dist/src/core/config.js +50 -0
  48. package/dist/src/core/errors.d.ts +34 -0
  49. package/dist/src/core/errors.js +70 -0
  50. package/dist/src/core/fusion.d.ts +20 -0
  51. package/dist/src/core/fusion.js +193 -0
  52. package/dist/src/core/logger.d.ts +22 -0
  53. package/dist/src/core/logger.js +49 -0
  54. package/dist/src/core/metrics.d.ts +48 -0
  55. package/dist/src/core/metrics.js +117 -0
  56. package/dist/src/core/native-adapter.d.ts +11 -0
  57. package/dist/src/core/native-adapter.js +211 -0
  58. package/dist/src/core/resources.d.ts +9 -0
  59. package/dist/src/core/resources.js +25 -0
  60. package/dist/src/core/scheduler.d.ts +34 -0
  61. package/dist/src/core/scheduler.js +85 -0
  62. package/dist/src/core/scope.d.ts +26 -0
  63. package/dist/src/core/scope.js +68 -0
  64. package/dist/src/core/serializer.d.ts +10 -0
  65. package/dist/src/core/serializer.js +44 -0
  66. package/dist/src/core/server.d.ts +138 -0
  67. package/dist/src/core/server.js +1082 -0
  68. package/dist/src/core/stream.d.ts +15 -0
  69. package/dist/src/core/stream.js +71 -0
  70. package/dist/src/core/tasks.d.ts +29 -0
  71. package/dist/src/core/tasks.js +87 -0
  72. package/dist/src/core/types.d.ts +173 -0
  73. package/dist/src/core/types.js +19 -0
  74. package/dist/src/core/websocket.d.ts +25 -0
  75. package/dist/src/core/websocket.js +86 -0
  76. package/dist/src/core/worker-queue.d.ts +41 -0
  77. package/dist/src/core/worker-queue.js +73 -0
  78. package/dist/src/cors.js +66 -0
  79. package/dist/src/database/adapters/memory.d.ts +21 -0
  80. package/dist/src/database/adapters/memory.js +90 -0
  81. package/dist/src/database/adapters/mongo.d.ts +11 -0
  82. package/dist/src/database/adapters/mongo.js +141 -0
  83. package/dist/src/database/adapters/postgres.d.ts +10 -0
  84. package/dist/src/database/adapters/postgres.js +111 -0
  85. package/dist/src/database/adapters/sqlite.d.ts +10 -0
  86. package/dist/src/database/adapters/sqlite.js +42 -0
  87. package/dist/src/database/coalescer.d.ts +14 -0
  88. package/dist/src/database/coalescer.js +134 -0
  89. package/dist/src/database/manager.d.ts +35 -0
  90. package/dist/src/database/manager.js +87 -0
  91. package/dist/src/database/types.d.ts +20 -0
  92. package/dist/src/database/types.js +2 -0
  93. package/dist/src/index.d.ts +50 -0
  94. package/dist/src/index.js +91 -0
  95. package/dist/src/logger.js +45 -0
  96. package/dist/src/metrics.js +111 -0
  97. package/dist/src/middleware/compression.d.ts +2 -0
  98. package/dist/src/middleware/compression.js +133 -0
  99. package/dist/src/middleware/cors.d.ts +2 -0
  100. package/dist/src/middleware/cors.js +66 -0
  101. package/dist/src/middleware/presets.d.ts +16 -0
  102. package/dist/src/middleware/presets.js +52 -0
  103. package/dist/src/middleware/rate-limit.d.ts +14 -0
  104. package/dist/src/middleware/rate-limit.js +83 -0
  105. package/dist/src/middleware/security.d.ts +21 -0
  106. package/dist/src/middleware/security.js +69 -0
  107. package/dist/src/middleware/static.d.ts +11 -0
  108. package/dist/src/middleware/static.js +191 -0
  109. package/dist/src/native/index.d.ts +32 -0
  110. package/dist/src/native/index.js +141 -0
  111. package/dist/src/openapi/generator.d.ts +19 -0
  112. package/dist/src/openapi/generator.js +149 -0
  113. package/dist/src/presets.js +33 -0
  114. package/dist/src/radix-router.js +89 -0
  115. package/dist/src/radix-tree.js +81 -0
  116. package/dist/src/resources.js +25 -0
  117. package/dist/src/router/radix-router.d.ts +18 -0
  118. package/dist/src/router/radix-router.js +89 -0
  119. package/dist/src/router/radix-tree.d.ts +18 -0
  120. package/dist/src/router/radix-tree.js +131 -0
  121. package/dist/src/router/router.d.ts +34 -0
  122. package/dist/src/router/router.js +186 -0
  123. package/dist/src/router.js +138 -0
  124. package/dist/src/scheduler.js +85 -0
  125. package/dist/src/security.js +69 -0
  126. package/dist/src/server.js +685 -0
  127. package/dist/src/signals.js +31 -0
  128. package/dist/src/static.js +107 -0
  129. package/dist/src/stream.js +71 -0
  130. package/dist/src/tasks.js +87 -0
  131. package/dist/src/testing/index.d.ts +25 -0
  132. package/dist/src/testing/index.js +84 -0
  133. package/dist/src/testing.js +40 -0
  134. package/dist/src/types.js +19 -0
  135. package/dist/src/utils/cookies.d.ts +3 -0
  136. package/dist/src/utils/cookies.js +59 -0
  137. package/dist/src/utils/logger.d.ts +12 -0
  138. package/dist/src/utils/logger.js +45 -0
  139. package/dist/src/utils/signals.d.ts +6 -0
  140. package/dist/src/utils/signals.js +31 -0
  141. package/dist/src/utils/sse.d.ts +6 -0
  142. package/dist/src/utils/sse.js +32 -0
  143. package/dist/src/utils/testing.js +40 -0
  144. package/dist/src/validation/index.d.ts +3 -0
  145. package/dist/src/validation/index.js +19 -0
  146. package/dist/src/validation/simple.d.ts +5 -0
  147. package/dist/src/validation/simple.js +102 -0
  148. package/dist/src/validation/types.d.ts +32 -0
  149. package/dist/src/validation/types.js +12 -0
  150. package/dist/src/validation/zod.d.ts +4 -0
  151. package/dist/src/validation/zod.js +18 -0
  152. package/dist/src/views/index.d.ts +1 -0
  153. package/dist/src/views/index.js +17 -0
  154. package/dist/src/views/types.d.ts +3 -0
  155. package/dist/src/views/types.js +2 -0
  156. package/dist/src/worker-queue.js +73 -0
  157. package/dist/tests/adapters.test.d.ts +1 -0
  158. package/dist/tests/adapters.test.js +106 -0
  159. package/dist/tests/batch.test.d.ts +1 -0
  160. package/dist/tests/batch.test.js +117 -0
  161. package/dist/tests/body-parser.test.d.ts +1 -0
  162. package/dist/tests/body-parser.test.js +52 -0
  163. package/dist/tests/compression-sse.test.d.ts +1 -0
  164. package/dist/tests/compression-sse.test.js +87 -0
  165. package/dist/tests/cookies.test.d.ts +1 -0
  166. package/dist/tests/cookies.test.js +63 -0
  167. package/dist/tests/cors.test.d.ts +1 -0
  168. package/dist/tests/cors.test.js +55 -0
  169. package/dist/tests/database.test.d.ts +1 -0
  170. package/dist/tests/database.test.js +80 -0
  171. package/dist/tests/dx.test.d.ts +1 -0
  172. package/dist/tests/dx.test.js +114 -0
  173. package/dist/tests/ecosystem.test.d.ts +1 -0
  174. package/dist/tests/ecosystem.test.js +133 -0
  175. package/dist/tests/features.test.d.ts +1 -0
  176. package/dist/tests/features.test.js +47 -0
  177. package/dist/tests/fusion.test.d.ts +1 -0
  178. package/dist/tests/fusion.test.js +92 -0
  179. package/dist/tests/http-basic.test.d.ts +1 -0
  180. package/dist/tests/http-basic.test.js +124 -0
  181. package/dist/tests/logger.test.d.ts +1 -0
  182. package/dist/tests/logger.test.js +33 -0
  183. package/dist/tests/middleware.test.d.ts +1 -0
  184. package/dist/tests/middleware.test.js +109 -0
  185. package/dist/tests/native-adapter.test.d.ts +1 -0
  186. package/dist/tests/native-adapter.test.js +71 -0
  187. package/dist/tests/observability.test.d.ts +1 -0
  188. package/dist/tests/observability.test.js +59 -0
  189. package/dist/tests/openapi.test.d.ts +1 -0
  190. package/dist/tests/openapi.test.js +64 -0
  191. package/dist/tests/plugin.test.d.ts +1 -0
  192. package/dist/tests/plugin.test.js +65 -0
  193. package/dist/tests/plugins.test.d.ts +1 -0
  194. package/dist/tests/plugins.test.js +71 -0
  195. package/dist/tests/rate-limit.test.d.ts +1 -0
  196. package/dist/tests/rate-limit.test.js +77 -0
  197. package/dist/tests/resources.test.d.ts +1 -0
  198. package/dist/tests/resources.test.js +47 -0
  199. package/dist/tests/scheduler.test.d.ts +1 -0
  200. package/dist/tests/scheduler.test.js +46 -0
  201. package/dist/tests/schema-routes.test.d.ts +1 -0
  202. package/dist/tests/schema-routes.test.js +77 -0
  203. package/dist/tests/security.test.d.ts +1 -0
  204. package/dist/tests/security.test.js +83 -0
  205. package/dist/tests/server-db.test.d.ts +1 -0
  206. package/dist/tests/server-db.test.js +72 -0
  207. package/dist/tests/smoke.test.d.ts +1 -0
  208. package/dist/tests/smoke.test.js +10 -0
  209. package/dist/tests/sqlite-fusion.test.d.ts +1 -0
  210. package/dist/tests/sqlite-fusion.test.js +92 -0
  211. package/dist/tests/static.test.d.ts +1 -0
  212. package/dist/tests/static.test.js +102 -0
  213. package/dist/tests/stream.test.d.ts +1 -0
  214. package/dist/tests/stream.test.js +44 -0
  215. package/dist/tests/task-metrics.test.d.ts +1 -0
  216. package/dist/tests/task-metrics.test.js +53 -0
  217. package/dist/tests/tasks.test.d.ts +1 -0
  218. package/dist/tests/tasks.test.js +62 -0
  219. package/dist/tests/testing.test.d.ts +1 -0
  220. package/dist/tests/testing.test.js +47 -0
  221. package/dist/tests/validation.test.d.ts +1 -0
  222. package/dist/tests/validation.test.js +107 -0
  223. package/dist/tests/websocket.test.d.ts +1 -0
  224. package/dist/tests/websocket.test.js +146 -0
  225. package/dist/vitest.config.d.ts +2 -0
  226. package/dist/vitest.config.js +9 -0
  227. package/docs/FUSION.md +19 -0
  228. package/package.json +2 -1
  229. package/prebuilds/darwin-arm64/qhttpx.node +0 -0
  230. package/prebuilds/linux-x64/qhttpx.node +0 -0
  231. package/prebuilds/win32-x64/qhttpx.node +0 -0
  232. package/scripts/install-native.js +26 -0
@@ -0,0 +1,102 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ const path_1 = __importDefault(require("path"));
8
+ const fs_1 = __importDefault(require("fs"));
9
+ // import http from 'http';
10
+ const server_1 = require("../src/core/server");
11
+ const static_1 = require("../src/middleware/static");
12
+ const TEST_DIR = path_1.default.join(__dirname, 'fixtures');
13
+ const TEST_FILE = path_1.default.join(TEST_DIR, 'test.txt');
14
+ const TEST_CONTENT = 'Hello World! This is a test file for QHTTPX static middleware.';
15
+ (0, vitest_1.describe)('static middleware', () => {
16
+ let app;
17
+ let serverUrl;
18
+ (0, vitest_1.beforeAll)(async () => {
19
+ // Create fixtures
20
+ if (!fs_1.default.existsSync(TEST_DIR))
21
+ fs_1.default.mkdirSync(TEST_DIR);
22
+ fs_1.default.writeFileSync(TEST_FILE, TEST_CONTENT);
23
+ app = new server_1.QHTTPX();
24
+ app.use((0, static_1.createStaticMiddleware)({ root: TEST_DIR }));
25
+ const { port } = await app.listen(0, '127.0.0.1');
26
+ serverUrl = `http://127.0.0.1:${port}`;
27
+ });
28
+ (0, vitest_1.afterAll)(async () => {
29
+ await app.close();
30
+ // Cleanup
31
+ if (fs_1.default.existsSync(TEST_FILE))
32
+ fs_1.default.unlinkSync(TEST_FILE);
33
+ if (fs_1.default.existsSync(TEST_DIR))
34
+ fs_1.default.rmdirSync(TEST_DIR);
35
+ });
36
+ (0, vitest_1.it)('should serve a static file', async () => {
37
+ const response = await fetch(`${serverUrl}/test.txt`);
38
+ (0, vitest_1.expect)(response.status).toBe(200);
39
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/plain');
40
+ const text = await response.text();
41
+ (0, vitest_1.expect)(text).toBe(TEST_CONTENT);
42
+ });
43
+ (0, vitest_1.it)('should support HEAD requests', async () => {
44
+ const response = await fetch(`${serverUrl}/test.txt`, { method: 'HEAD' });
45
+ (0, vitest_1.expect)(response.status).toBe(200);
46
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/plain');
47
+ (0, vitest_1.expect)(response.headers.get('content-length')).toBe(TEST_CONTENT.length.toString());
48
+ const text = await response.text();
49
+ (0, vitest_1.expect)(text).toBe('');
50
+ });
51
+ (0, vitest_1.it)('should support Range requests (first byte)', async () => {
52
+ const response = await fetch(`${serverUrl}/test.txt`, {
53
+ headers: { Range: 'bytes=0-0' }
54
+ });
55
+ (0, vitest_1.expect)(response.status).toBe(206);
56
+ (0, vitest_1.expect)(response.headers.get('content-range')).toBe(`bytes 0-0/${TEST_CONTENT.length}`);
57
+ (0, vitest_1.expect)(response.headers.get('content-length')).toBe('1');
58
+ const text = await response.text();
59
+ (0, vitest_1.expect)(text).toBe('H');
60
+ });
61
+ (0, vitest_1.it)('should support Range requests (partial)', async () => {
62
+ const response = await fetch(`${serverUrl}/test.txt`, {
63
+ headers: { Range: 'bytes=0-4' }
64
+ });
65
+ (0, vitest_1.expect)(response.status).toBe(206);
66
+ (0, vitest_1.expect)(response.headers.get('content-range')).toBe(`bytes 0-4/${TEST_CONTENT.length}`);
67
+ (0, vitest_1.expect)(response.headers.get('content-length')).toBe('5');
68
+ const text = await response.text();
69
+ (0, vitest_1.expect)(text).toBe('Hello');
70
+ });
71
+ (0, vitest_1.it)('should return 416 for invalid Range', async () => {
72
+ const response = await fetch(`${serverUrl}/test.txt`, {
73
+ headers: { Range: 'bytes=1000-2000' }
74
+ });
75
+ (0, vitest_1.expect)(response.status).toBe(416);
76
+ (0, vitest_1.expect)(response.headers.get('content-range')).toBe(`bytes */${TEST_CONTENT.length}`);
77
+ });
78
+ (0, vitest_1.it)('should support ETag caching (304 Not Modified)', async () => {
79
+ const response1 = await fetch(`${serverUrl}/test.txt`);
80
+ const etag = response1.headers.get('etag');
81
+ (0, vitest_1.expect)(etag).toBeTruthy();
82
+ const response2 = await fetch(`${serverUrl}/test.txt`, {
83
+ headers: { 'If-None-Match': etag }
84
+ });
85
+ (0, vitest_1.expect)(response2.status).toBe(304);
86
+ const text = await response2.text();
87
+ (0, vitest_1.expect)(text).toBe('');
88
+ });
89
+ (0, vitest_1.it)('should support Last-Modified caching (304 Not Modified)', async () => {
90
+ const response1 = await fetch(`${serverUrl}/test.txt`);
91
+ const lastModified = response1.headers.get('last-modified');
92
+ (0, vitest_1.expect)(lastModified).toBeTruthy();
93
+ const response2 = await fetch(`${serverUrl}/test.txt`, {
94
+ headers: { 'If-Modified-Since': lastModified }
95
+ });
96
+ (0, vitest_1.expect)(response2.status).toBe(304);
97
+ });
98
+ (0, vitest_1.it)('should return 404 for non-existent file', async () => {
99
+ const response = await fetch(`${serverUrl}/does-not-exist.txt`);
100
+ (0, vitest_1.expect)(response.status).toBe(404);
101
+ });
102
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const stream_1 = require("stream");
4
+ const vitest_1 = require("vitest");
5
+ const src_1 = require("../src");
6
+ (0, vitest_1.describe)('Streaming helpers', () => {
7
+ let app;
8
+ (0, vitest_1.afterEach)(async () => {
9
+ if (app) {
10
+ await app.close();
11
+ app = undefined;
12
+ }
13
+ });
14
+ (0, vitest_1.it)('sends a simple SSE event', async () => {
15
+ app = new src_1.QHTTPX({ requestTimeoutMs: 5000 });
16
+ app.get('/sse', (ctx) => {
17
+ const sse = (0, src_1.createSseStream)(ctx);
18
+ sse.send({ message: 'hello' });
19
+ sse.close();
20
+ });
21
+ const { port } = await app.listen(0, '127.0.0.1');
22
+ const response = await fetch(`http://127.0.0.1:${port}/sse`);
23
+ (0, vitest_1.expect)(response.status).toBe(200);
24
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/event-stream');
25
+ const text = await response.text();
26
+ (0, vitest_1.expect)(text.trim()).toBe('data: {"message":"hello"}');
27
+ });
28
+ (0, vitest_1.it)('streams data from a readable', async () => {
29
+ app = new src_1.QHTTPX();
30
+ app.get('/stream', async (ctx) => {
31
+ const readable = stream_1.Readable.from(['hello', ' ', 'world']);
32
+ await (0, src_1.sendStream)(ctx, readable, {
33
+ contentType: 'text/plain; charset=utf-8',
34
+ status: 200,
35
+ });
36
+ });
37
+ const { port } = await app.listen(0, '127.0.0.1');
38
+ const response = await fetch(`http://127.0.0.1:${port}/stream`);
39
+ (0, vitest_1.expect)(response.status).toBe(200);
40
+ (0, vitest_1.expect)(response.headers.get('content-type')).toContain('text/plain');
41
+ const text = await response.text();
42
+ (0, vitest_1.expect)(text).toBe('hello world');
43
+ });
44
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../src/index");
5
+ (0, vitest_1.describe)('Task Metrics', () => {
6
+ let app;
7
+ (0, vitest_1.afterEach)(async () => {
8
+ if (app) {
9
+ await app.close();
10
+ app = undefined;
11
+ }
12
+ });
13
+ (0, vitest_1.it)('exposes task metrics in /__qhttpx/metrics', async () => {
14
+ app = new index_1.QHTTPX();
15
+ // Register a simple task
16
+ app.task('simple-task', async () => {
17
+ // do nothing
18
+ });
19
+ const { port } = await app.listen(0, '127.0.0.1');
20
+ // Enqueue the task a few times
21
+ await app.enqueue('simple-task', {});
22
+ await app.enqueue('simple-task', {});
23
+ // Allow some time for async tasks to complete
24
+ await new Promise((resolve) => setTimeout(resolve, 50));
25
+ const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
26
+ (0, vitest_1.expect)(response.status).toBe(200);
27
+ const json = (await response.json());
28
+ (0, vitest_1.expect)(json.tasks).toBeDefined();
29
+ (0, vitest_1.expect)(json.tasks.registeredTasks).toBe(1);
30
+ (0, vitest_1.expect)(json.tasks.totalEnqueued).toBe(2);
31
+ (0, vitest_1.expect)(json.tasks.totalCompleted).toBe(2);
32
+ (0, vitest_1.expect)(json.tasks.totalFailed).toBe(0);
33
+ (0, vitest_1.expect)(json.tasks.totalOverloaded).toBe(0);
34
+ });
35
+ (0, vitest_1.it)('counts failed tasks', async () => {
36
+ app = new index_1.QHTTPX();
37
+ app.task('failing-task', async () => {
38
+ throw new Error('Task failed');
39
+ }, { maxRetries: 1 }); // 1 retry
40
+ const { port } = await app.listen(0, '127.0.0.1');
41
+ try {
42
+ await app.enqueue('failing-task', {});
43
+ }
44
+ catch {
45
+ }
46
+ // Allow time for retry and failure
47
+ await new Promise((resolve) => setTimeout(resolve, 50));
48
+ const response = await fetch(`http://127.0.0.1:${port}/__qhttpx/metrics`);
49
+ const json = (await response.json());
50
+ (0, vitest_1.expect)(json.tasks.totalFailed).toBe(1);
51
+ (0, vitest_1.expect)(json.tasks.totalRetried).toBe(1); // 1 retry attempt
52
+ });
53
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const index_1 = require("../src/index");
5
+ (0, vitest_1.describe)('Task engine', () => {
6
+ (0, vitest_1.it)('registers and executes a task', async () => {
7
+ const app = new index_1.QHTTPX();
8
+ const seen = [];
9
+ app.task('echo', async (payload) => {
10
+ seen.push(payload);
11
+ });
12
+ await app.enqueue('echo', { hello: 'world' });
13
+ (0, vitest_1.expect)(seen).toHaveLength(1);
14
+ (0, vitest_1.expect)(seen[0]).toEqual({ hello: 'world' });
15
+ });
16
+ (0, vitest_1.it)('retries failing tasks according to maxRetries', async () => {
17
+ const app = new index_1.QHTTPX();
18
+ const spy = vitest_1.vi.fn();
19
+ let attempts = 0;
20
+ app.task('sometimes-fails', () => {
21
+ spy();
22
+ attempts += 1;
23
+ if (attempts < 3) {
24
+ throw new Error('fail');
25
+ }
26
+ }, { maxRetries: 5 });
27
+ await app.enqueue('sometimes-fails', null);
28
+ (0, vitest_1.expect)(spy).toHaveBeenCalledTimes(3);
29
+ });
30
+ (0, vitest_1.it)('propagates errors after exceeding retries', async () => {
31
+ const app = new index_1.QHTTPX();
32
+ app.task('always-fails', () => {
33
+ throw new Error('boom');
34
+ }, { maxRetries: 1 });
35
+ await (0, vitest_1.expect)(app.enqueue('always-fails', null)).rejects.toThrow('boom');
36
+ });
37
+ (0, vitest_1.it)('integrates with HTTP route that enqueues a job', async () => {
38
+ const app = new index_1.QHTTPX();
39
+ const seen = [];
40
+ app.task('job', async (payload) => {
41
+ seen.push(payload);
42
+ });
43
+ app.post('/enqueue', async (ctx) => {
44
+ const body = ctx.body;
45
+ await app.enqueue('job', body.value);
46
+ ctx.json({ ok: true });
47
+ });
48
+ const { port } = await app.listen(0, '127.0.0.1');
49
+ const response = await fetch(`http://127.0.0.1:${port}/enqueue`, {
50
+ method: 'POST',
51
+ headers: {
52
+ 'content-type': 'application/json',
53
+ },
54
+ body: JSON.stringify({ value: 'hello' }),
55
+ });
56
+ (0, vitest_1.expect)(response.status).toBe(200);
57
+ const json = (await response.json());
58
+ (0, vitest_1.expect)(json.ok).toBe(true);
59
+ (0, vitest_1.expect)(seen).toEqual(['hello']);
60
+ await app.close();
61
+ });
62
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const src_1 = require("../src");
5
+ (0, vitest_1.describe)('Testing utilities', () => {
6
+ (0, vitest_1.it)('creates a test client and handles requests', async () => {
7
+ const app = new src_1.QHTTPX();
8
+ app.get('/hello', (ctx) => {
9
+ ctx.json({ message: 'world' });
10
+ });
11
+ app.post('/echo', (ctx) => {
12
+ ctx.json(ctx.body);
13
+ });
14
+ const client = (0, src_1.createTestClient)(app);
15
+ try {
16
+ const res1 = await client.get('/hello');
17
+ (0, vitest_1.expect)(res1.status).toBe(200);
18
+ (0, vitest_1.expect)(await res1.json()).toEqual({ message: 'world' });
19
+ const res2 = await client.post('/echo', { foo: 'bar' });
20
+ (0, vitest_1.expect)(res2.status).toBe(200);
21
+ (0, vitest_1.expect)(await res2.json()).toEqual({ foo: 'bar' });
22
+ }
23
+ finally {
24
+ await client.stop();
25
+ }
26
+ });
27
+ (0, vitest_1.it)('handles custom headers and raw body', async () => {
28
+ const app = new src_1.QHTTPX();
29
+ app.post('/raw', (ctx) => {
30
+ ctx.send(ctx.req.headers['x-custom']);
31
+ });
32
+ const client = (0, src_1.createTestClient)(app);
33
+ try {
34
+ const res = await client.post('/raw', 'raw-body', {
35
+ headers: {
36
+ 'x-custom': 'custom-value',
37
+ 'content-type': 'text/plain',
38
+ },
39
+ });
40
+ (0, vitest_1.expect)(res.status).toBe(200);
41
+ (0, vitest_1.expect)(await res.text()).toBe('custom-value');
42
+ }
43
+ finally {
44
+ await client.stop();
45
+ }
46
+ });
47
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const server_1 = require("../src/core/server");
5
+ (0, vitest_1.describe)('Validation System', () => {
6
+ let app;
7
+ (0, vitest_1.afterEach)(async () => {
8
+ if (app)
9
+ await app.close();
10
+ });
11
+ (0, vitest_1.it)('should validate request body and return 400 on failure', async () => {
12
+ app = new server_1.QHTTPX();
13
+ const schema = {
14
+ body: {
15
+ type: 'object',
16
+ properties: {
17
+ name: { type: 'string', min: 3 },
18
+ age: { type: 'number', min: 18 }
19
+ }
20
+ }
21
+ };
22
+ app.post('/user', {
23
+ schema,
24
+ handler: (ctx) => {
25
+ ctx.json({ status: 'ok', body: ctx.body });
26
+ }
27
+ });
28
+ const { port } = await app.listen(0);
29
+ // Invalid request (age too low)
30
+ const res1 = await fetch(`http://localhost:${port}/user`, {
31
+ method: 'POST',
32
+ headers: { 'Content-Type': 'application/json' },
33
+ body: JSON.stringify({ name: 'Alice', age: 10 })
34
+ });
35
+ (0, vitest_1.expect)(res1.status).toBe(400);
36
+ const data1 = await res1.json();
37
+ (0, vitest_1.expect)(data1.error).toBe('Validation Error');
38
+ (0, vitest_1.expect)(data1.details).toContain('age');
39
+ // Valid request
40
+ const res2 = await fetch(`http://localhost:${port}/user`, {
41
+ method: 'POST',
42
+ headers: { 'Content-Type': 'application/json' },
43
+ body: JSON.stringify({ name: 'Alice', age: 20 })
44
+ });
45
+ (0, vitest_1.expect)(res2.status).toBe(200);
46
+ const data2 = await res2.json();
47
+ (0, vitest_1.expect)(data2.body).toEqual({ name: 'Alice', age: 20 });
48
+ });
49
+ (0, vitest_1.it)('should validate and coerce query parameters', async () => {
50
+ app = new server_1.QHTTPX();
51
+ const schema = {
52
+ query: {
53
+ type: 'object',
54
+ properties: {
55
+ page: { type: 'number', min: 1 },
56
+ sort: { type: 'string', enum: ['asc', 'desc'] }
57
+ }
58
+ }
59
+ };
60
+ app.get('/items', {
61
+ schema,
62
+ handler: (ctx) => {
63
+ // ctx.query should have numbers now
64
+ ctx.json({
65
+ page: ctx.query.page,
66
+ pageType: typeof ctx.query.page,
67
+ sort: ctx.query.sort
68
+ });
69
+ }
70
+ });
71
+ const { port } = await app.listen(0);
72
+ // Valid query
73
+ const res1 = await fetch(`http://localhost:${port}/items?page=5&sort=desc`);
74
+ (0, vitest_1.expect)(res1.status).toBe(200);
75
+ const data1 = await res1.json();
76
+ (0, vitest_1.expect)(data1.page).toBe(5);
77
+ (0, vitest_1.expect)(data1.pageType).toBe('number'); // Coercion worked
78
+ (0, vitest_1.expect)(data1.sort).toBe('desc');
79
+ // Invalid query
80
+ const res2 = await fetch(`http://localhost:${port}/items?page=0&sort=foo`);
81
+ (0, vitest_1.expect)(res2.status).toBe(400);
82
+ });
83
+ (0, vitest_1.it)('should support legacy response schema (no validation)', async () => {
84
+ app = new server_1.QHTTPX();
85
+ // Legacy schema: just a JSON schema at root
86
+ const schema = {
87
+ type: 'object',
88
+ properties: {
89
+ hello: { type: 'string' }
90
+ }
91
+ };
92
+ app.get('/legacy', {
93
+ schema,
94
+ handler: (ctx) => {
95
+ // If validation ran, it might fail because ctx.body is undefined or not matching
96
+ // But here we check if it IGNORES validation and uses it for serialization
97
+ ctx.json({ hello: 'world', extra: 'hidden' });
98
+ }
99
+ });
100
+ const { port } = await app.listen(0);
101
+ const res = await fetch(`http://localhost:${port}/legacy`);
102
+ (0, vitest_1.expect)(res.status).toBe(200);
103
+ const data = await res.json();
104
+ (0, vitest_1.expect)(data.hello).toBe('world');
105
+ (0, vitest_1.expect)(data.extra).toBeUndefined(); // fast-json-stringify filters extra fields
106
+ });
107
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const vitest_1 = require("vitest");
40
+ const net = __importStar(require("net"));
41
+ const ws_1 = __importDefault(require("ws"));
42
+ const src_1 = require("../src");
43
+ (0, vitest_1.describe)('WebSocket upgrade', () => {
44
+ (0, vitest_1.it)('handles websocket upgrade for registered path', async () => {
45
+ const app = new src_1.QHTTPX();
46
+ let upgraded = false;
47
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
48
+ app.upgrade('/ws', (ws, req) => {
49
+ upgraded = true;
50
+ ws.close();
51
+ });
52
+ const { port } = await app.listen(0, '127.0.0.1');
53
+ const ws = new ws_1.default(`ws://127.0.0.1:${port}/ws`);
54
+ await new Promise((resolve, reject) => {
55
+ ws.on('open', () => {
56
+ resolve();
57
+ });
58
+ ws.on('error', (err) => {
59
+ reject(err);
60
+ });
61
+ });
62
+ (0, vitest_1.expect)(upgraded).toBe(true);
63
+ ws.close();
64
+ await app.close();
65
+ });
66
+ (0, vitest_1.it)('destroys socket when no upgrade handler is registered for path', async () => {
67
+ const app = new src_1.QHTTPX();
68
+ const { port } = await app.listen(0, '127.0.0.1');
69
+ const socket = net.createConnection({ port, host: '127.0.0.1' });
70
+ const done = new Promise((resolve) => {
71
+ let finished = false;
72
+ const finish = () => {
73
+ if (!finished) {
74
+ finished = true;
75
+ resolve();
76
+ }
77
+ };
78
+ socket.on('end', finish);
79
+ socket.on('close', finish);
80
+ socket.on('error', () => {
81
+ finish();
82
+ });
83
+ setTimeout(finish, 500);
84
+ });
85
+ const requestLines = [
86
+ 'GET /no-ws HTTP/1.1',
87
+ 'Host: 127.0.0.1',
88
+ 'Upgrade: websocket',
89
+ 'Connection: Upgrade',
90
+ '',
91
+ '',
92
+ ];
93
+ socket.write(requestLines.join('\r\n'));
94
+ await done;
95
+ await app.close();
96
+ });
97
+ (0, vitest_1.it)('supports rooms and broadcasting', async () => {
98
+ const app = new src_1.QHTTPX();
99
+ app.upgrade('/chat', (ws, req) => {
100
+ const url = new URL(req.url || '', 'http://localhost');
101
+ const room = url.searchParams.get('room');
102
+ if (room) {
103
+ ws.join(room);
104
+ }
105
+ });
106
+ app.post('/broadcast', (ctx) => {
107
+ const { room, message } = ctx.body;
108
+ app.websocket.to(room).emit(message);
109
+ ctx.json({ success: true });
110
+ });
111
+ const { port } = await app.listen(0, '127.0.0.1');
112
+ const baseUrl = `ws://127.0.0.1:${port}/chat`;
113
+ // Client A in room1
114
+ const wsA = new ws_1.default(`${baseUrl}?room=room1`);
115
+ const msgsA = [];
116
+ wsA.on('message', (data) => msgsA.push(data.toString()));
117
+ // Client B in room1
118
+ const wsB = new ws_1.default(`${baseUrl}?room=room1`);
119
+ const msgsB = [];
120
+ wsB.on('message', (data) => msgsB.push(data.toString()));
121
+ // Client C in room2
122
+ const wsC = new ws_1.default(`${baseUrl}?room=room2`);
123
+ const msgsC = [];
124
+ wsC.on('message', (data) => msgsC.push(data.toString()));
125
+ await Promise.all([
126
+ new Promise((resolve) => wsA.on('open', () => resolve())),
127
+ new Promise((resolve) => wsB.on('open', () => resolve())),
128
+ new Promise((resolve) => wsC.on('open', () => resolve())),
129
+ ]);
130
+ // Broadcast to room1
131
+ // We need to make a POST request.
132
+ // Since we are inside test, we can use fetch or http.request.
133
+ // Or just use app.websocket directly if we can access it?
134
+ // We can access app.websocket directly here since we have app instance.
135
+ app.websocket.to('room1').emit('hello room1');
136
+ // Wait a bit for messages
137
+ await new Promise((resolve) => setTimeout(resolve, 100));
138
+ (0, vitest_1.expect)(msgsA).toContain('hello room1');
139
+ (0, vitest_1.expect)(msgsB).toContain('hello room1');
140
+ (0, vitest_1.expect)(msgsC).not.toContain('hello room1');
141
+ wsA.close();
142
+ wsB.close();
143
+ wsC.close();
144
+ await app.close();
145
+ });
146
+ });
@@ -0,0 +1,2 @@
1
+ declare const _default: import("vite").UserConfig;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const config_1 = require("vitest/config");
4
+ exports.default = (0, config_1.defineConfig)({
5
+ test: {
6
+ include: ['tests/**/*.test.ts'],
7
+ exclude: ['dist/**', 'node_modules/**'],
8
+ },
9
+ });
package/docs/FUSION.md CHANGED
@@ -66,3 +66,22 @@ For even higher performance, Fusion includes a "Micro-TTL" mechanism. Even after
66
66
 
67
67
  - **Idempotency**: Fusion is safest for `GET` requests. QHTTPX automatically disables Fusion for `POST`, `PUT`, `DELETE`, and `PATCH` by default to prevent side-effect duplication issues, though this can be overridden if you know what you are doing.
68
68
  - **Personalized Content**: Be careful with endpoints that return user-specific data (like "My Profile"). If the response depends on the `Authorization` header, ensure that header is part of the Fusion Key (or disable Fusion for that route).
69
+
70
+ ## Observability
71
+
72
+ You can verify Request Fusion effectiveness using the built-in metrics endpoint.
73
+
74
+ GET `/__qhttpx/metrics`
75
+
76
+ Response:
77
+ ```json
78
+ {
79
+ "totalRequests": 10000,
80
+ "fusion": {
81
+ "totalFused": 9999, // Requests served by Leader/Cache
82
+ "totalProcessed": 1 // Actual handler executions
83
+ }
84
+ }
85
+ ```
86
+
87
+ This confirms that out of 10,000 requests, only 1 actually hit your database/handler!
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qhttpx",
3
- "version": "1.9.2",
3
+ "version": "1.9.3",
4
4
  "gypfile": false,
5
5
  "description": "The Ultra-Fast HTTP Framework for Node.js",
6
6
  "main": "dist/src/index.js",
@@ -31,6 +31,7 @@
31
31
  },
32
32
  "scripts": {
33
33
  "prebuild": "node -e \"try{require('child_process').execSync('prebuildify --napi --strip', {stdio: 'ignore'})}catch(e){}\"",
34
+ "prepublishOnly": "npm run build",
34
35
  "build": "tsc -p tsconfig.json",
35
36
  "lint": "eslint src tests --ext .ts",
36
37
  "test": "vitest run",
Binary file
File without changes
Binary file