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,190 @@
1
+ import { RadixTree } from './radix-tree';
2
+ import { HTTPMethod, QHTTPXHandler, RouteOptions, RoutePriority } from '../core/types';
3
+ import { RouteSchema } from '../validation/types';
4
+
5
+ type RouteDefinition = {
6
+ path: string;
7
+ segments: string[];
8
+ handler: QHTTPXHandler;
9
+ priority: RoutePriority;
10
+ schema?: RouteSchema | Record<string, unknown>;
11
+ };
12
+
13
+ export type RouteMatch = {
14
+ handler: QHTTPXHandler;
15
+ params: Record<string, string>;
16
+ priority: RoutePriority;
17
+ };
18
+
19
+ export class Router {
20
+ // Per-method route buckets
21
+ private readonly methodBuckets: Map<HTTPMethod, RouteDefinition[]> = new Map([
22
+ ['GET', []],
23
+ ['POST', []],
24
+ ['PUT', []],
25
+ ['DELETE', []],
26
+ ['PATCH', []],
27
+ ['HEAD', []],
28
+ ['OPTIONS', []],
29
+ ]);
30
+
31
+ // Derived structures (built at freeze time)
32
+ private readonly radixTrees: Map<HTTPMethod, RadixTree> = new Map();
33
+
34
+ // Freeze state
35
+ private isFrozen = false;
36
+
37
+ register(
38
+ method: HTTPMethod,
39
+ path: string,
40
+ handler: QHTTPXHandler,
41
+ options?: RouteOptions & { schema?: RouteSchema | Record<string, unknown> },
42
+ ): void {
43
+ if (this.isFrozen) {
44
+ console.warn(
45
+ `Router is frozen. Late route registration (${method} ${path}) may not be optimized.`,
46
+ );
47
+ }
48
+
49
+ const segments = this.normalize(path);
50
+ const bucket = this.methodBuckets.get(method);
51
+ if (bucket) {
52
+ bucket.push({
53
+ path,
54
+ segments,
55
+ handler,
56
+ priority: options?.priority ?? RoutePriority.STANDARD,
57
+ schema: options?.schema,
58
+ });
59
+ }
60
+ }
61
+
62
+ getRoutes(): Map<HTTPMethod, RouteDefinition[]> {
63
+ return this.methodBuckets;
64
+ }
65
+
66
+ match(method: HTTPMethod, path: string): RouteMatch | undefined {
67
+ // Fast path for frozen router
68
+ if (this.isFrozen) {
69
+ const tree = this.radixTrees.get(method);
70
+ if (tree) {
71
+ const segments = this.normalize(path);
72
+ const match = tree.lookup(segments);
73
+ if (match) {
74
+ return match;
75
+ }
76
+ }
77
+ return undefined;
78
+ }
79
+
80
+ // Slow path for unfrozen router (legacy behavior)
81
+ const segments = this.normalize(path);
82
+ const bucket = this.methodBuckets.get(method);
83
+
84
+ if (!bucket || bucket.length === 0) {
85
+ return undefined;
86
+ }
87
+
88
+ // Try to match against routes
89
+ for (const route of bucket) {
90
+ if (route.segments.length !== segments.length) {
91
+ continue;
92
+ }
93
+
94
+ const params: Record<string, string> = {};
95
+ let matched = true;
96
+
97
+ for (let i = 0; i < route.segments.length; i += 1) {
98
+ const pattern = route.segments[i];
99
+ const value = segments[i];
100
+
101
+ if (pattern.startsWith(':')) {
102
+ const key = pattern.slice(1);
103
+ params[key] = value;
104
+ } else if (pattern !== value) {
105
+ matched = false;
106
+ break;
107
+ }
108
+ }
109
+
110
+ if (matched) {
111
+ return {
112
+ handler: route.handler,
113
+ params,
114
+ priority: route.priority,
115
+ };
116
+ }
117
+ }
118
+
119
+ return undefined;
120
+ }
121
+
122
+ getAllowedMethods(path: string): HTTPMethod[] {
123
+ const segments = this.normalize(path);
124
+ const methods: HTTPMethod[] = [];
125
+
126
+ for (const [method, routes] of this.methodBuckets.entries()) {
127
+ for (const route of routes) {
128
+ if (route.segments.length !== segments.length) {
129
+ continue;
130
+ }
131
+
132
+ let matched = true;
133
+ for (let i = 0; i < route.segments.length; i += 1) {
134
+ const pattern = route.segments[i];
135
+ const value = segments[i];
136
+
137
+ if (pattern.startsWith(':')) {
138
+ continue;
139
+ }
140
+
141
+ if (pattern !== value) {
142
+ matched = false;
143
+ break;
144
+ }
145
+ }
146
+
147
+ if (matched) {
148
+ methods.push(method);
149
+ break;
150
+ }
151
+ }
152
+ }
153
+
154
+ return methods;
155
+ }
156
+
157
+ /**
158
+ * Freeze the router after server starts.
159
+ * Prevents further route registration and builds derived structures for optimized matching.
160
+ */
161
+ freeze(): void {
162
+ if (this.isFrozen) {
163
+ return;
164
+ }
165
+
166
+ this.isFrozen = true;
167
+
168
+ // Build derived structures for faster matching
169
+ for (const [method, routes] of this.methodBuckets.entries()) {
170
+ const tree = new RadixTree();
171
+
172
+ for (const route of routes) {
173
+ tree.insert(route.segments, route.handler, route.priority);
174
+ }
175
+
176
+ this.radixTrees.set(method, tree);
177
+ }
178
+ }
179
+
180
+ isFrozenRouter(): boolean {
181
+ return this.isFrozen;
182
+ }
183
+
184
+ private normalize(path: string): string[] {
185
+ if (!path || path === '/') {
186
+ return [];
187
+ }
188
+ return path.split('/').filter((segment) => segment.length > 0);
189
+ }
190
+ }
@@ -0,0 +1,104 @@
1
+ import { QHTTPX } from '../core/server';
2
+ import { HTTPMethod } from '../core/types';
3
+
4
+ export class TestClient {
5
+ private app: QHTTPX;
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ private server: any = null;
8
+ private baseURL: string = '';
9
+
10
+ constructor(app: QHTTPX) {
11
+ this.app = app;
12
+ }
13
+
14
+ /**
15
+ * Starts the server on a random port.
16
+ * Automatically called by request methods if not started.
17
+ */
18
+ async start(): Promise<void> {
19
+ if (this.server) return;
20
+ const { port } = await this.app.listen(0);
21
+ this.server = this.app.serverInstance;
22
+ this.baseURL = `http://127.0.0.1:${port}`;
23
+ }
24
+
25
+ async stop(): Promise<void> {
26
+ if (this.server) {
27
+ await this.app.close();
28
+ this.server = null;
29
+ }
30
+ }
31
+
32
+ async request(
33
+ method: HTTPMethod,
34
+ path: string,
35
+ options: {
36
+ headers?: Record<string, string>;
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ body?: any;
39
+ query?: Record<string, string | number | boolean>;
40
+ } = {}
41
+ ): Promise<Response> {
42
+ if (!this.server) {
43
+ await this.start();
44
+ }
45
+
46
+ const url = new URL(path, this.baseURL);
47
+ if (options.query) {
48
+ Object.entries(options.query).forEach(([k, v]) => {
49
+ url.searchParams.append(k, String(v));
50
+ });
51
+ }
52
+
53
+ const headers: Record<string, string> = options.headers || {};
54
+ let bodyPayload: BodyInit | undefined;
55
+
56
+ if (options.body) {
57
+ if (typeof options.body === 'object' &&
58
+ !(options.body instanceof Uint8Array) &&
59
+ !(options.body instanceof ArrayBuffer) &&
60
+ !(options.body instanceof FormData) &&
61
+ !(options.body instanceof URLSearchParams)) {
62
+ bodyPayload = JSON.stringify(options.body);
63
+ if (!headers['content-type']) {
64
+ headers['content-type'] = 'application/json';
65
+ }
66
+ } else {
67
+ bodyPayload = options.body as BodyInit;
68
+ }
69
+ }
70
+
71
+ return fetch(url, {
72
+ method,
73
+ headers,
74
+ body: bodyPayload,
75
+ });
76
+ }
77
+
78
+ get(path: string, options?: Omit<Parameters<TestClient['request']>[2], 'body'>) {
79
+ return this.request('GET', path, options);
80
+ }
81
+
82
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
83
+ post(path: string, body?: any, options?: Omit<Parameters<TestClient['request']>[2], 'body'>) {
84
+ return this.request('POST', path, { ...options, body });
85
+ }
86
+
87
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
88
+ put(path: string, body?: any, options?: Omit<Parameters<TestClient['request']>[2], 'body'>) {
89
+ return this.request('PUT', path, { ...options, body });
90
+ }
91
+
92
+ delete(path: string, options?: Omit<Parameters<TestClient['request']>[2], 'body'>) {
93
+ return this.request('DELETE', path, options);
94
+ }
95
+
96
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
97
+ patch(path: string, body?: any, options?: Omit<Parameters<TestClient['request']>[2], 'body'>) {
98
+ return this.request('PATCH', path, { ...options, body });
99
+ }
100
+ }
101
+
102
+ export function createTestClient(app: QHTTPX) {
103
+ return new TestClient(app);
104
+ }
@@ -0,0 +1,67 @@
1
+ import { CookieOptions } from '../core/types';
2
+
3
+ export function parseCookies(header: string | undefined): Record<string, string> {
4
+ const list: Record<string, string> = {};
5
+ if (!header) {
6
+ return list;
7
+ }
8
+
9
+ header.split(';').forEach((cookie) => {
10
+ const parts = cookie.split('=');
11
+ const name = parts.shift()?.trim();
12
+ if (name) {
13
+ const value = parts.join('=');
14
+ list[name] = decodeURIComponent(value);
15
+ }
16
+ });
17
+
18
+ return list;
19
+ }
20
+
21
+ export function serializeCookie(name: string, value: string, options: CookieOptions = {}): string {
22
+ let str = `${name}=${encodeURIComponent(value)}`;
23
+
24
+ if (options.maxAge) {
25
+ str += `; Max-Age=${Math.floor(options.maxAge)}`;
26
+ }
27
+
28
+ if (options.domain) {
29
+ str += `; Domain=${options.domain}`;
30
+ }
31
+
32
+ if (options.path) {
33
+ str += `; Path=${options.path}`;
34
+ } else {
35
+ str += '; Path=/';
36
+ }
37
+
38
+ if (options.expires) {
39
+ str += `; Expires=${options.expires.toUTCString()}`;
40
+ }
41
+
42
+ if (options.httpOnly) {
43
+ str += '; HttpOnly';
44
+ }
45
+
46
+ if (options.secure) {
47
+ str += '; Secure';
48
+ }
49
+
50
+ if (options.sameSite) {
51
+ switch (options.sameSite) {
52
+ case 'lax':
53
+ str += '; SameSite=Lax';
54
+ break;
55
+ case 'strict':
56
+ str += '; SameSite=Strict';
57
+ break;
58
+ case 'none':
59
+ str += '; SameSite=None';
60
+ break;
61
+ default:
62
+ break;
63
+ }
64
+ }
65
+
66
+ return str;
67
+ }
@@ -0,0 +1,59 @@
1
+ import { QHTTPXContext, QHTTPXMiddleware } from '../core/types';
2
+
3
+ export type LogEntry = {
4
+ method: string;
5
+ path: string;
6
+ status: number;
7
+ durationMs: number;
8
+ requestId?: string;
9
+ };
10
+
11
+ export type LoggerOptions = {
12
+ sink?: (entry: LogEntry, ctx: QHTTPXContext) => void;
13
+ };
14
+
15
+ export function createLoggerMiddleware(
16
+ options: LoggerOptions = {},
17
+ ): QHTTPXMiddleware {
18
+ const sink =
19
+ options.sink ??
20
+ ((entry: LogEntry) => {
21
+ const status = entry.status;
22
+ let color = '\x1b[37m';
23
+ if (status >= 200 && status < 300) {
24
+ color = '\x1b[32m';
25
+ } else if (status === 404) {
26
+ color = '\x1b[33m';
27
+ } else if (status >= 500) {
28
+ color = '\x1b[34m';
29
+ } else if (status >= 400) {
30
+ color = '\x1b[35m';
31
+ }
32
+ const reset = '\x1b[0m';
33
+ const prefix = entry.requestId ? `${entry.requestId} ` : '';
34
+ const line = `${prefix}${entry.method} ${entry.path} ${status} ${entry.durationMs}ms`;
35
+ console.log(`${color}${line}${reset}`);
36
+ });
37
+
38
+ return async (ctx, next) => {
39
+ const start = ctx.requestStart ?? Date.now();
40
+ try {
41
+ await next();
42
+ } finally {
43
+ const durationMs = Math.max(1, Date.now() - start);
44
+ const method = ctx.req.method || 'GET';
45
+ const path = ctx.url.pathname;
46
+ const status = ctx.res.statusCode || 200;
47
+ sink(
48
+ {
49
+ method,
50
+ path,
51
+ status,
52
+ durationMs,
53
+ requestId: ctx.requestId,
54
+ },
55
+ ctx,
56
+ );
57
+ }
58
+ };
59
+ }
@@ -0,0 +1,45 @@
1
+ import { QHTTPX } from '../core/server';
2
+
3
+ export type SignalHandlerOptions = {
4
+ signals?: NodeJS.Signals[];
5
+ timeoutMs?: number;
6
+ };
7
+
8
+ export function attachSignalHandlers(
9
+ app: QHTTPX,
10
+ options: SignalHandlerOptions = {},
11
+ ): void {
12
+ const signals = options.signals ?? ['SIGINT', 'SIGTERM'];
13
+ const timeoutMs = options.timeoutMs ?? 10000;
14
+
15
+ let shuttingDown = false;
16
+
17
+ const handler = async (signal: NodeJS.Signals) => {
18
+ if (shuttingDown) {
19
+ return;
20
+ }
21
+ shuttingDown = true;
22
+
23
+ console.log(`Received ${signal}, shutting down...`);
24
+
25
+ const timer = setTimeout(() => {
26
+ console.error(
27
+ 'Shutdown timed out, forcing exit. (some connections might be lost)',
28
+ );
29
+ process.exit(1);
30
+ }, timeoutMs);
31
+
32
+ try {
33
+ await app.shutdown();
34
+ clearTimeout(timer);
35
+ process.exit(0);
36
+ } catch (err) {
37
+ console.error('Error during shutdown:', err);
38
+ process.exit(1);
39
+ }
40
+ };
41
+
42
+ for (const signal of signals) {
43
+ process.on(signal, handler);
44
+ }
45
+ }
@@ -0,0 +1,41 @@
1
+ import { QHTTPXContext } from '../core/types';
2
+
3
+ export interface SSEStream {
4
+ send(data: unknown, event?: string, id?: string): void;
5
+ close(): void;
6
+ }
7
+
8
+ export function createSSE(ctx: QHTTPXContext): SSEStream {
9
+ const res = ctx.res;
10
+
11
+ // Disable auto-end so we can keep the connection open
12
+ ctx.disableAutoEnd = true;
13
+
14
+ // Only write headers if not already sent
15
+ if (!res.headersSent) {
16
+ res.setHeader('Content-Type', 'text/event-stream');
17
+ res.setHeader('Cache-Control', 'no-cache');
18
+ res.setHeader('Connection', 'keep-alive');
19
+ res.setHeader('X-Accel-Buffering', 'no'); // For Nginx
20
+
21
+ // Send initial ping/comment to flush headers and establish connection
22
+ res.write(': connected\n\n');
23
+ }
24
+
25
+ const send = (data: unknown, event?: string, id?: string) => {
26
+ if (id) res.write(`id: ${id}\n`);
27
+ if (event) res.write(`event: ${event}\n`);
28
+ const payload = typeof data === 'string' ? data : JSON.stringify(data);
29
+ res.write(`data: ${payload}\n\n`);
30
+ };
31
+
32
+ const close = () => {
33
+ res.end();
34
+ };
35
+
36
+ ctx.req.on('close', () => {
37
+ // console.log('SSE client disconnected');
38
+ });
39
+
40
+ return { send, close };
41
+ }
@@ -0,0 +1,3 @@
1
+ export * from './types';
2
+ export * from './simple';
3
+ export * from './zod';
@@ -0,0 +1,93 @@
1
+ import { SimpleSchema, Validator, ValidationResult, ValidationError } from './types';
2
+
3
+ export class SimpleValidator implements Validator {
4
+ validate(schema: unknown, data: unknown): ValidationResult {
5
+ try {
6
+ // If schema is not a SimpleSchema object, we can't validate it with this validator
7
+ // But for simplicity, we assume the user passes a valid SimpleSchema if they use this validator.
8
+ const validData = this.check(schema as SimpleSchema, data);
9
+ return { success: true, data: validData };
10
+ } catch (err) {
11
+ return {
12
+ success: false,
13
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
14
+ error: new ValidationError((err as any).message || 'Validation failed')
15
+ };
16
+ }
17
+ }
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ private check(schema: SimpleSchema, data: unknown, path: string = ''): any {
21
+ if (schema.required !== false && (data === undefined || data === null)) {
22
+ throw new Error(`Field '${path}' is required`);
23
+ }
24
+
25
+ if (data === undefined || data === null) {
26
+ return data;
27
+ }
28
+
29
+ switch (schema.type) {
30
+ case 'string':
31
+ if (typeof data !== 'string') throw new Error(`Field '${path}' must be a string`);
32
+ if (schema.min !== undefined && data.length < schema.min) throw new Error(`Field '${path}' too short (min ${schema.min})`);
33
+ if (schema.max !== undefined && data.length > schema.max) throw new Error(`Field '${path}' too long (max ${schema.max})`);
34
+ if (schema.pattern && !new RegExp(schema.pattern).test(data)) throw new Error(`Field '${path}' format invalid`);
35
+ if (schema.enum && !schema.enum.includes(data)) throw new Error(`Field '${path}' must be one of [${schema.enum.join(', ')}]`);
36
+ return data;
37
+
38
+ case 'number':
39
+ // Try to coerce if it's a string looking like a number (useful for query params)
40
+ let num = data;
41
+ if (typeof data === 'string' && !isNaN(Number(data))) {
42
+ num = Number(data);
43
+ }
44
+
45
+ if (typeof num !== 'number' || isNaN(num as number)) throw new Error(`Field '${path}' must be a number`);
46
+ if (schema.min !== undefined && (num as number) < schema.min) throw new Error(`Field '${path}' too small (min ${schema.min})`);
47
+ if (schema.max !== undefined && (num as number) > schema.max) throw new Error(`Field '${path}' too large (max ${schema.max})`);
48
+ if (schema.enum && !schema.enum.includes(num as number)) throw new Error(`Field '${path}' must be one of [${schema.enum.join(', ')}]`);
49
+ return num;
50
+
51
+ case 'boolean':
52
+ if (typeof data === 'boolean') return data;
53
+ if (data === 'true') return true;
54
+ if (data === 'false') return false;
55
+ throw new Error(`Field '${path}' must be a boolean`);
56
+
57
+ case 'array':
58
+ if (!Array.isArray(data)) throw new Error(`Field '${path}' must be an array`);
59
+ if (schema.min !== undefined && data.length < schema.min) throw new Error(`Field '${path}' too few items (min ${schema.min})`);
60
+ if (schema.max !== undefined && data.length > schema.max) throw new Error(`Field '${path}' too many items (max ${schema.max})`);
61
+ if (schema.items) {
62
+ return data.map((item, i) => this.check(schema.items!, item, `${path}[${i}]`));
63
+ }
64
+ return data;
65
+
66
+ case 'object':
67
+ if (typeof data !== 'object' || Array.isArray(data)) throw new Error(`Field '${path}' must be an object`);
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
+ const result: any = {};
70
+ const props = schema.properties || {};
71
+
72
+ // Check known properties
73
+ for (const key in props) {
74
+ const propSchema = props[key];
75
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
76
+ const propValue = (data as any)[key];
77
+ result[key] = this.check(propSchema, propValue, path ? `${path}.${key}` : key);
78
+ }
79
+
80
+ // Pass through unknown properties?
81
+ // For strictness, maybe we should strip them?
82
+ // Let's keep unknown properties for now to be safe, or make it configurable.
83
+ // For now: Only keep validated properties if properties are defined.
84
+ if (Object.keys(props).length > 0) {
85
+ return result;
86
+ }
87
+ return data;
88
+
89
+ default:
90
+ return data;
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,38 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export type ValidationResult<T = any> =
3
+ | { success: true; data: T }
4
+ | { success: false; error: ValidationError };
5
+
6
+ export class ValidationError extends Error {
7
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8
+ constructor(public details: any) {
9
+ super('Validation Error');
10
+ this.name = 'ValidationError';
11
+ }
12
+ }
13
+
14
+ export interface Validator {
15
+ validate(schema: unknown, data: unknown): Promise<ValidationResult> | ValidationResult;
16
+ }
17
+
18
+ export type RouteSchema = {
19
+ body?: unknown;
20
+ query?: unknown;
21
+ params?: unknown;
22
+ headers?: unknown;
23
+ response?: unknown;
24
+ };
25
+
26
+ // Simple Schema Types for the built-in validator
27
+ export type SimpleType = 'string' | 'number' | 'boolean' | 'object' | 'array';
28
+
29
+ export interface SimpleSchema {
30
+ type: SimpleType;
31
+ required?: boolean; // default true
32
+ properties?: Record<string, SimpleSchema>; // for object
33
+ items?: SimpleSchema; // for array
34
+ min?: number; // min length or value
35
+ max?: number; // max length or value
36
+ pattern?: string; // regex for string
37
+ enum?: (string | number)[];
38
+ }
@@ -0,0 +1,14 @@
1
+ import { Validator, ValidationResult, ValidationError } from './types';
2
+
3
+ export class ZodValidator implements Validator {
4
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
5
+ async validate<T = any>(schema: unknown, data: unknown): Promise<ValidationResult<T>> {
6
+ try {
7
+ const zodSchema = schema as { parseAsync: (data: unknown) => Promise<T> };
8
+ const result = await zodSchema.parseAsync(data);
9
+ return { success: true, data: result };
10
+ } catch (error) {
11
+ return { success: false, error: new ValidationError(error) };
12
+ }
13
+ }
14
+ }
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,4 @@
1
+ export interface ViewEngine {
2
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
3
+ render(path: string, locals: Record<string, any>): Promise<string> | string;
4
+ }