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,222 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __importDefault = (this && this.__importDefault) || function (mod) {
4
+ return (mod && mod.__esModule) ? mod : { "default": mod };
5
+ };
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ const child_process_1 = require("child_process");
8
+ const cluster_1 = __importDefault(require("cluster"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const fs_1 = __importDefault(require("fs"));
12
+ const args = process.argv.slice(2);
13
+ const command = args[0];
14
+ if (!command) {
15
+ printHelp();
16
+ process.exit(1);
17
+ }
18
+ const targetFile = args[1];
19
+ if (command === 'start') {
20
+ if (!targetFile) {
21
+ console.error('Error: Please provide an entry file (e.g., qhttpx start dist/index.js)');
22
+ process.exit(1);
23
+ }
24
+ runStart(targetFile);
25
+ }
26
+ else if (command === 'dev') {
27
+ if (!targetFile) {
28
+ console.error('Error: Please provide an entry file (e.g., qhttpx dev src/index.ts)');
29
+ process.exit(1);
30
+ }
31
+ runDev(targetFile);
32
+ }
33
+ else if (command === 'new') {
34
+ if (!targetFile) {
35
+ console.error('Error: Please provide a project name (e.g., qhttpx new my-app)');
36
+ process.exit(1);
37
+ }
38
+ runNew(targetFile);
39
+ }
40
+ else {
41
+ console.error(`Unknown command: ${command}`);
42
+ printHelp();
43
+ process.exit(1);
44
+ }
45
+ function printHelp() {
46
+ console.log('Usage: qhttpx <command> [options]');
47
+ console.log('Commands:');
48
+ console.log(' start <file> Run in production cluster mode (Node.js)');
49
+ console.log(' dev <file> Run in development mode with hot reload (tsx)');
50
+ console.log(' new <name> Create a new QHTTPX project');
51
+ }
52
+ function runStart(entry) {
53
+ const absoluteEntry = path_1.default.resolve(process.cwd(), entry);
54
+ if (cluster_1.default.isPrimary) {
55
+ const numCPUs = os_1.default.cpus().length;
56
+ console.log(`[QCLI] 🚀 Starting Cluster Mode`);
57
+ console.log(`[QCLI] 📂 Entry: ${absoluteEntry}`);
58
+ console.log(`[QCLI] 💻 Workers: ${numCPUs}`);
59
+ for (let i = 0; i < numCPUs; i++) {
60
+ cluster_1.default.fork();
61
+ }
62
+ cluster_1.default.on('exit', (worker) => {
63
+ console.log(`[QCLI] 💀 Worker ${worker.process.pid} died. Restarting...`);
64
+ cluster_1.default.fork();
65
+ });
66
+ }
67
+ else {
68
+ try {
69
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
70
+ require(absoluteEntry);
71
+ }
72
+ catch (err) {
73
+ console.error(`[QCLI] Failed to load entry file: ${absoluteEntry}`);
74
+ console.error(err);
75
+ process.exit(1);
76
+ }
77
+ }
78
+ }
79
+ function runDev(entry) {
80
+ const absoluteEntry = path_1.default.resolve(process.cwd(), entry);
81
+ console.log(`[QCLI] 🛠️ Starting Dev Mode`);
82
+ console.log(`[QCLI] 📂 Entry: ${absoluteEntry}`);
83
+ let child = null;
84
+ const startChild = () => {
85
+ // Use tsx for dev execution
86
+ child = (0, child_process_1.spawn)('npx', ['tsx', absoluteEntry], {
87
+ stdio: 'inherit',
88
+ shell: true,
89
+ cwd: process.cwd()
90
+ });
91
+ child.on('close', (code) => {
92
+ if (code !== 0 && code !== null) {
93
+ console.log(`[QCLI] App crashed (code ${code}). Waiting for changes...`);
94
+ }
95
+ });
96
+ };
97
+ const restartChild = () => {
98
+ if (child) {
99
+ console.log('[QCLI] 🔄 File changed. Restarting...');
100
+ // On Windows, killing a shell-spawned process is tricky.
101
+ // But 'child.kill()' usually works for the handle.
102
+ // For deeper tree killing, we might need a library, but let's try standard kill.
103
+ child.kill();
104
+ child = null;
105
+ }
106
+ startChild();
107
+ };
108
+ startChild();
109
+ // Watcher
110
+ let debounceTimer;
111
+ const watchDir = path_1.default.dirname(absoluteEntry); // Watch the directory of the entry file (usually src)
112
+ console.log(`[QCLI] 👀 Watching: ${watchDir}`);
113
+ // Recursive watch is platform dependent, but 'recursive: true' works on Windows/macOS
114
+ try {
115
+ fs_1.default.watch(watchDir, { recursive: true }, (eventType, filename) => {
116
+ if (!filename)
117
+ return;
118
+ // Ignore node_modules and .git
119
+ if (filename.includes('node_modules') || filename.includes('.git'))
120
+ return;
121
+ clearTimeout(debounceTimer);
122
+ debounceTimer = setTimeout(() => {
123
+ restartChild();
124
+ }, 200); // 200ms debounce
125
+ });
126
+ }
127
+ catch {
128
+ console.warn('[QCLI] Recursive watch not supported or failed. Falling back to simple watch.');
129
+ // Fallback or just fail gracefully
130
+ }
131
+ }
132
+ function runNew(projectName) {
133
+ const projectDir = path_1.default.resolve(process.cwd(), projectName);
134
+ if (fs_1.default.existsSync(projectDir)) {
135
+ console.error(`Error: Directory ${projectName} already exists.`);
136
+ process.exit(1);
137
+ }
138
+ console.log(`[QCLI] ✨ Creating new QHTTPX project in ${projectName}...`);
139
+ fs_1.default.mkdirSync(projectDir);
140
+ fs_1.default.mkdirSync(path_1.default.join(projectDir, 'src'));
141
+ // package.json
142
+ const pkgJson = {
143
+ name: projectName,
144
+ version: '0.0.1',
145
+ type: 'module',
146
+ scripts: {
147
+ dev: 'qhttpx dev src/index.ts',
148
+ start: 'qhttpx start dist/index.js',
149
+ build: 'tsc'
150
+ },
151
+ dependencies: {
152
+ 'qhttpx': '^1.8.6',
153
+ 'dotenv': '^16.4.1'
154
+ },
155
+ devDependencies: {
156
+ 'typescript': '^5.3.3',
157
+ '@types/node': '^20.11.16',
158
+ 'tsx': '^4.7.0'
159
+ }
160
+ };
161
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, 'package.json'), JSON.stringify(pkgJson, null, 2));
162
+ // tsconfig.json
163
+ const tsConfig = {
164
+ compilerOptions: {
165
+ target: 'ES2022',
166
+ module: 'Node16',
167
+ moduleResolution: 'Node16',
168
+ rootDir: 'src',
169
+ outDir: 'dist',
170
+ strict: true,
171
+ esModuleInterop: true,
172
+ skipLibCheck: true,
173
+ forceConsistentCasingInFileNames: true
174
+ },
175
+ include: ['src']
176
+ };
177
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, 'tsconfig.json'), JSON.stringify(tsConfig, null, 2));
178
+ // src/index.ts
179
+ const indexTs = `import { createHttpApp } from 'qhttpx';
180
+ import 'dotenv/config';
181
+
182
+ const app = createHttpApp({
183
+ // ⚡ Performance: Auto-coalesce duplicate requests
184
+ enableRequestFusion: true,
185
+
186
+ // 🛡️ Security: Built-in CORS & Rate Limiting
187
+ cors: true,
188
+ rateLimit: {
189
+ windowMs: 15 * 60 * 1000, // 15 min
190
+ max: 100, // 100 reqs per IP
191
+ trustProxy: true
192
+ },
193
+
194
+ // 🗜️ Optimization: Gzip/Brotli compression
195
+ compression: true
196
+ });
197
+
198
+ app.get('/', ({ json }) => {
199
+ json({
200
+ message: 'Welcome to QHTTPX 🚀',
201
+ status: 'online',
202
+ timestamp: Date.now()
203
+ });
204
+ });
205
+
206
+ app.listen(Number(process.env.PORT) || 3000, () => {
207
+ console.log(\`Server running on http://localhost:\${process.env.PORT || 3000}\`);
208
+ });
209
+ `;
210
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, 'src', 'index.ts'), indexTs);
211
+ // .gitignore
212
+ const gitignore = `node_modules
213
+ dist
214
+ .env
215
+ `;
216
+ fs_1.default.writeFileSync(path_1.default.join(projectDir, '.gitignore'), gitignore);
217
+ console.log(`[QCLI] ✅ Project created successfully!`);
218
+ console.log(`\nNext steps:`);
219
+ console.log(` cd ${projectName}`);
220
+ console.log(` npm install`);
221
+ console.log(` npm run dev`);
222
+ }
@@ -0,0 +1,17 @@
1
+ export type InferRequestType<T> = T extends {
2
+ request: infer R;
3
+ } ? R : never;
4
+ export type InferResponseType<T> = T extends {
5
+ response: infer R;
6
+ } ? R : never;
7
+ export declare function hc<T extends Record<string, any>>(baseUrl: string, options?: RequestInit): Client<T>;
8
+ type Client<T> = {
9
+ [K in keyof T]: T[K] extends Function ? never : Client<T[K]>;
10
+ } & {
11
+ $get: (args?: any) => Promise<any>;
12
+ $post: (args?: any) => Promise<any>;
13
+ $put: (args?: any) => Promise<any>;
14
+ $delete: (args?: any) => Promise<any>;
15
+ $patch: (args?: any) => Promise<any>;
16
+ };
17
+ export {};
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-function-type */
4
+ // RPC Client for QHTTPX
5
+ // Allows end-to-end type safety similar to Hono's 'hc' or Elysia's 'eden'
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.hc = hc;
8
+ // Proxy-based client generator
9
+ function hc(baseUrl, options = {}) {
10
+ const handler = {
11
+ get(target, prop, receiver) {
12
+ if (typeof prop !== 'string')
13
+ return Reflect.get(target, prop, receiver);
14
+ // If it's a method call like .$get()
15
+ if (prop.startsWith('$')) {
16
+ const method = prop.slice(1).toUpperCase();
17
+ return async (args = {}) => {
18
+ let url = baseUrl;
19
+ const path = target.__path || '';
20
+ // Replace params in path :id
21
+ let finalPath = path;
22
+ if (args.param) {
23
+ Object.entries(args.param).forEach(([key, value]) => {
24
+ finalPath = finalPath.replace(`:${key}`, String(value));
25
+ });
26
+ }
27
+ url += finalPath;
28
+ // Append query params
29
+ if (args.query) {
30
+ const searchParams = new URLSearchParams();
31
+ Object.entries(args.query).forEach(([key, value]) => {
32
+ searchParams.append(key, String(value));
33
+ });
34
+ url += `?${searchParams.toString()}`;
35
+ }
36
+ const fetchOptions = {
37
+ ...options,
38
+ method,
39
+ headers: {
40
+ ...options.headers,
41
+ ...(args.header || {}),
42
+ ...(args.json ? { 'Content-Type': 'application/json' } : {}),
43
+ },
44
+ };
45
+ if (args.json) {
46
+ fetchOptions.body = JSON.stringify(args.json);
47
+ }
48
+ else if (args.form) {
49
+ // Handle FormData
50
+ fetchOptions.body = args.form;
51
+ }
52
+ const res = await fetch(url, fetchOptions);
53
+ // Return a wrapper that allows typed access
54
+ return {
55
+ ok: res.ok,
56
+ status: res.status,
57
+ statusText: res.statusText,
58
+ headers: res.headers,
59
+ json: () => res.json(),
60
+ text: () => res.text(),
61
+ blob: () => res.blob(),
62
+ };
63
+ };
64
+ }
65
+ // It's a path segment
66
+ const newPath = (target.__path || '') + '/' + prop;
67
+ const proxy = new Proxy({ __path: newPath }, handler);
68
+ return proxy;
69
+ }
70
+ };
71
+ return new Proxy({ __path: '' }, handler);
72
+ }
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadConfig = loadConfig;
4
+ function loadConfig(options = {}) {
5
+ const env = options.env ?? process.env;
6
+ const defaults = options.defaults ?? {};
7
+ const prefix = options.prefix ?? 'QHTTPX_';
8
+ const getNumber = (key) => {
9
+ const value = env[prefix + key];
10
+ if (!value) {
11
+ return undefined;
12
+ }
13
+ const n = Number(value);
14
+ if (!Number.isFinite(n) || n <= 0) {
15
+ return undefined;
16
+ }
17
+ return n;
18
+ };
19
+ const getBoolean = (key) => {
20
+ const value = env[prefix + key];
21
+ if (!value) {
22
+ return undefined;
23
+ }
24
+ const lower = value.toLowerCase();
25
+ if (lower === 'true' || lower === '1') {
26
+ return true;
27
+ }
28
+ if (lower === 'false' || lower === '0') {
29
+ return false;
30
+ }
31
+ return undefined;
32
+ };
33
+ const maxConcurrency = getNumber('MAX_CONCURRENCY');
34
+ const requestTimeoutMs = getNumber('REQUEST_TIMEOUT_MS');
35
+ const maxMemoryBytes = getNumber('MAX_MEMORY_BYTES');
36
+ const maxBodyBytes = getNumber('MAX_BODY_BYTES');
37
+ const keepAliveTimeoutMs = getNumber('KEEP_ALIVE_TIMEOUT_MS');
38
+ const headersTimeoutMs = getNumber('HEADERS_TIMEOUT_MS');
39
+ const metricsEnabled = getBoolean('METRICS_ENABLED');
40
+ return {
41
+ ...defaults,
42
+ maxConcurrency: maxConcurrency ?? defaults.maxConcurrency,
43
+ requestTimeoutMs: requestTimeoutMs ?? defaults.requestTimeoutMs,
44
+ maxMemoryBytes: maxMemoryBytes ?? defaults.maxMemoryBytes,
45
+ maxBodyBytes: maxBodyBytes ?? defaults.maxBodyBytes,
46
+ keepAliveTimeoutMs: keepAliveTimeoutMs ?? defaults.keepAliveTimeoutMs,
47
+ headersTimeoutMs: headersTimeoutMs ?? defaults.headersTimeoutMs,
48
+ metricsEnabled: metricsEnabled ?? defaults.metricsEnabled,
49
+ };
50
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseCookies = parseCookies;
4
+ exports.serializeCookie = serializeCookie;
5
+ function parseCookies(header) {
6
+ const list = {};
7
+ if (!header) {
8
+ return list;
9
+ }
10
+ header.split(';').forEach((cookie) => {
11
+ const parts = cookie.split('=');
12
+ const name = parts.shift()?.trim();
13
+ if (name) {
14
+ const value = parts.join('=');
15
+ list[name] = decodeURIComponent(value);
16
+ }
17
+ });
18
+ return list;
19
+ }
20
+ function serializeCookie(name, value, options = {}) {
21
+ let str = `${name}=${encodeURIComponent(value)}`;
22
+ if (options.maxAge) {
23
+ str += `; Max-Age=${Math.floor(options.maxAge)}`;
24
+ }
25
+ if (options.domain) {
26
+ str += `; Domain=${options.domain}`;
27
+ }
28
+ if (options.path) {
29
+ str += `; Path=${options.path}`;
30
+ }
31
+ else {
32
+ str += '; Path=/';
33
+ }
34
+ if (options.expires) {
35
+ str += `; Expires=${options.expires.toUTCString()}`;
36
+ }
37
+ if (options.httpOnly) {
38
+ str += '; HttpOnly';
39
+ }
40
+ if (options.secure) {
41
+ str += '; Secure';
42
+ }
43
+ if (options.sameSite) {
44
+ switch (options.sameSite) {
45
+ case 'lax':
46
+ str += '; SameSite=Lax';
47
+ break;
48
+ case 'strict':
49
+ str += '; SameSite=Strict';
50
+ break;
51
+ case 'none':
52
+ str += '; SameSite=None';
53
+ break;
54
+ default:
55
+ break;
56
+ }
57
+ }
58
+ return str;
59
+ }
@@ -0,0 +1,24 @@
1
+ import { QHTTPXContext, QHTTPXOpHandler } from './types';
2
+ import { DatabaseManager } from '../database/manager';
3
+ export declare class BatchExecutor {
4
+ private ops;
5
+ private dbManager?;
6
+ private coalescers;
7
+ constructor(dbManager?: DatabaseManager);
8
+ register(name: string, handler: QHTTPXOpHandler): void;
9
+ handleBatch(ctx: QHTTPXContext, batch: Array<{
10
+ op: string;
11
+ params: unknown;
12
+ id?: unknown;
13
+ }>): Promise<{
14
+ results: ({
15
+ error: string;
16
+ id: unknown;
17
+ result?: undefined;
18
+ } | {
19
+ result: any;
20
+ id: unknown;
21
+ error?: undefined;
22
+ })[];
23
+ }>;
24
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BatchExecutor = void 0;
4
+ const coalescer_1 = require("../database/coalescer");
5
+ class BatchExecutor {
6
+ constructor(dbManager) {
7
+ this.ops = new Map();
8
+ this.coalescers = new Map();
9
+ this.dbManager = dbManager;
10
+ }
11
+ register(name, handler) {
12
+ this.ops.set(name, handler);
13
+ }
14
+ async handleBatch(ctx, batch) {
15
+ if (!Array.isArray(batch)) {
16
+ throw new Error('Batch must be an array');
17
+ }
18
+ // If we have a DB manager, we want to intercept queries for fusion
19
+ // We create a proxy for the DB manager attached to the context
20
+ let batchCtx = ctx;
21
+ if (this.dbManager && ctx.db) {
22
+ // Create a localized context that intercepts DB calls
23
+ // We need to ensure that when `ctx.db.get()` is called, it returns a wrapped adapter
24
+ // But `ctx.db` is the manager itself.
25
+ // We can't easily wrap the manager instance shared across requests.
26
+ // Instead, we can wrap the `ctx.db` property for this request scope.
27
+ const originalDb = ctx.db;
28
+ // Create a proxy for the database manager
29
+ const dbProxy = new Proxy(originalDb, {
30
+ get: (target, prop, receiver) => {
31
+ if (prop === 'get' || prop === 'connect') {
32
+ return (name) => {
33
+ const adapter = target.get(name);
34
+ // Check if we have a coalescer for this connection
35
+ const connName = name || 'default'; // Simplify for now
36
+ let coalescer = this.coalescers.get(connName);
37
+ // In a real per-request batch scope, we should create a new Coalescer
38
+ // OR reset the existing one?
39
+ // Actually, the Coalescer uses a short timeout (next tick).
40
+ // So it is safe to share across concurrent requests IF we want global batching.
41
+ // BUT "auto transaction scope" implies per-request isolation.
42
+ // For "Query Fusion", global batching is even better (cross-request fusion).
43
+ // However, for this task, let's stick to per-request or global safely.
44
+ // Since Coalescer flushes on next tick, sharing it is fine and efficient.
45
+ if (!coalescer) {
46
+ coalescer = new coalescer_1.QueryCoalescer(adapter);
47
+ this.coalescers.set(connName, coalescer);
48
+ }
49
+ // Return a proxy to the adapter that uses the coalescer
50
+ return {
51
+ ...adapter,
52
+ query: (query, params) => coalescer.query(query, params),
53
+ isConnected: () => adapter.isConnected(),
54
+ connect: () => adapter.connect(),
55
+ disconnect: () => adapter.disconnect()
56
+ };
57
+ };
58
+ }
59
+ return Reflect.get(target, prop, receiver);
60
+ }
61
+ });
62
+ // Create a new context object that inherits from original but overrides db
63
+ // We use Object.create to prototype inherit, but ctx is a flat object in QHTTPX.
64
+ // We must shallow copy.
65
+ batchCtx = { ...ctx, db: dbProxy };
66
+ }
67
+ // Helper for internal calls
68
+ // "ctx.call" implementation
69
+ // The user wants: `ctx.call("getUser", { id: 5 })`
70
+ // We attach this to the context
71
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
+ batchCtx.call = async (op, params) => {
73
+ const handler = this.ops.get(op);
74
+ if (!handler) {
75
+ throw new Error(`Unknown operation: ${op}`);
76
+ }
77
+ return handler(params, batchCtx);
78
+ };
79
+ // Execute all ops
80
+ // We can run them in parallel since we want to fuse queries
81
+ const results = await Promise.all(batch.map(async (item) => {
82
+ try {
83
+ const handler = this.ops.get(item.op);
84
+ if (!handler) {
85
+ return { error: `Unknown operation: ${item.op}`, id: item.id };
86
+ }
87
+ const result = await handler(item.params, batchCtx);
88
+ return { result, id: item.id };
89
+ }
90
+ catch (err) {
91
+ return { error: (err instanceof Error ? err.message : String(err)), id: item.id };
92
+ }
93
+ }));
94
+ return { results };
95
+ }
96
+ }
97
+ exports.BatchExecutor = BatchExecutor;
@@ -0,0 +1,15 @@
1
+ import { IncomingMessage } from 'http';
2
+ export type BodyParserOptions = {
3
+ maxBodyBytes?: number;
4
+ };
5
+ export type ParsedBody = {
6
+ body: unknown;
7
+ files?: Record<string, unknown>;
8
+ };
9
+ export interface IBodyParser {
10
+ parse(req: IncomingMessage, options?: BodyParserOptions): Promise<any>;
11
+ }
12
+ export declare class BodyParser {
13
+ static parse(req: IncomingMessage, options?: BodyParserOptions): Promise<any>;
14
+ private static parseMultipart;
15
+ }
@@ -0,0 +1,121 @@
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
+ exports.BodyParser = void 0;
7
+ const querystring_1 = require("querystring");
8
+ const busboy_1 = __importDefault(require("busboy"));
9
+ class BodyParser {
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ static async parse(req, options = {}) {
12
+ const method = (req.method || 'GET').toUpperCase();
13
+ if (method === 'GET' || method === 'HEAD') {
14
+ return { body: undefined };
15
+ }
16
+ const contentType = req.headers['content-type'] || '';
17
+ if (contentType.includes('multipart/form-data')) {
18
+ return this.parseMultipart(req, options);
19
+ }
20
+ const chunks = [];
21
+ let totalBytes = 0;
22
+ const maxBodyBytes = options.maxBodyBytes;
23
+ for await (const chunk of req) {
24
+ const bufferChunk = typeof chunk === 'string' ? Buffer.from(chunk) : chunk;
25
+ chunks.push(bufferChunk);
26
+ totalBytes += bufferChunk.length;
27
+ if (maxBodyBytes !== undefined && totalBytes > maxBodyBytes) {
28
+ throw new Error('QHTTPX_BODY_TOO_LARGE');
29
+ }
30
+ }
31
+ if (chunks.length === 0) {
32
+ return { body: undefined };
33
+ }
34
+ const buffer = Buffer.concat(chunks);
35
+ if (typeof contentType === 'string' &&
36
+ contentType.includes('application/json')) {
37
+ const text = buffer.toString('utf8');
38
+ try {
39
+ return { body: JSON.parse(text) };
40
+ }
41
+ catch {
42
+ throw new Error('QHTTPX_INVALID_JSON');
43
+ }
44
+ }
45
+ if (typeof contentType === 'string' &&
46
+ contentType.includes('application/x-www-form-urlencoded')) {
47
+ const text = buffer.toString('utf8');
48
+ return { body: (0, querystring_1.parse)(text) };
49
+ }
50
+ return { body: buffer };
51
+ }
52
+ static parseMultipart(req, options) {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ return new Promise((resolve, reject) => {
55
+ let bb;
56
+ try {
57
+ bb = (0, busboy_1.default)({ headers: req.headers, limits: { fileSize: options.maxBodyBytes } });
58
+ }
59
+ catch (err) {
60
+ return reject(err);
61
+ }
62
+ const body = {};
63
+ const files = {};
64
+ bb.on('field', (name, val) => {
65
+ if (Object.prototype.hasOwnProperty.call(body, name)) {
66
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
+ if (Array.isArray(body[name])) {
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ body[name].push(val);
70
+ }
71
+ else {
72
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
+ body[name] = [body[name], val];
74
+ }
75
+ }
76
+ else {
77
+ body[name] = val;
78
+ }
79
+ });
80
+ bb.on('file', (name, file, info) => {
81
+ const { filename, encoding, mimeType } = info;
82
+ const chunks = [];
83
+ file.on('data', (chunk) => {
84
+ chunks.push(chunk);
85
+ });
86
+ file.on('end', () => {
87
+ const buffer = Buffer.concat(chunks);
88
+ const fileData = {
89
+ filename,
90
+ encoding,
91
+ mimeType,
92
+ data: buffer,
93
+ size: buffer.length,
94
+ };
95
+ if (Object.prototype.hasOwnProperty.call(files, name)) {
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ if (Array.isArray(files[name])) {
98
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
+ files[name].push(fileData);
100
+ }
101
+ else {
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ files[name] = [files[name], fileData];
104
+ }
105
+ }
106
+ else {
107
+ files[name] = fileData;
108
+ }
109
+ });
110
+ });
111
+ bb.on('close', () => {
112
+ resolve({ body, files });
113
+ });
114
+ bb.on('error', (err) => {
115
+ reject(err);
116
+ });
117
+ req.pipe(bb);
118
+ });
119
+ }
120
+ }
121
+ exports.BodyParser = BodyParser;