qhttpx 1.8.1 → 1.8.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 (98) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +114 -276
  3. package/assets/logo.svg +25 -0
  4. package/dist/package.json +39 -6
  5. package/dist/src/benchmarks/quantam-users.d.ts +1 -0
  6. package/dist/src/benchmarks/simple-json.d.ts +1 -0
  7. package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
  8. package/dist/src/cli/index.d.ts +2 -0
  9. package/dist/src/client/index.d.ts +17 -0
  10. package/dist/src/core/batch.d.ts +24 -0
  11. package/dist/src/core/body-parser.d.ts +15 -0
  12. package/dist/src/core/buffer-pool.d.ts +41 -0
  13. package/dist/src/core/config.d.ts +7 -0
  14. package/dist/src/core/fusion.d.ts +14 -0
  15. package/dist/src/core/logger.d.ts +22 -0
  16. package/dist/src/core/metrics.d.ts +45 -0
  17. package/dist/src/core/resources.d.ts +9 -0
  18. package/dist/src/core/scheduler.d.ts +34 -0
  19. package/dist/src/core/scope.d.ts +26 -0
  20. package/dist/src/core/serializer.d.ts +10 -0
  21. package/dist/src/core/server.d.ts +86 -0
  22. package/dist/src/core/server.js +122 -94
  23. package/dist/src/core/stream.d.ts +15 -0
  24. package/dist/src/core/tasks.d.ts +29 -0
  25. package/dist/src/core/types.d.ts +134 -0
  26. package/dist/src/core/websocket.d.ts +25 -0
  27. package/dist/src/core/worker-queue.d.ts +41 -0
  28. package/dist/src/database/adapters/memory.d.ts +21 -0
  29. package/dist/src/database/adapters/mongo.d.ts +11 -0
  30. package/dist/src/database/adapters/postgres.d.ts +10 -0
  31. package/dist/src/database/adapters/sqlite.d.ts +10 -0
  32. package/dist/src/database/coalescer.d.ts +14 -0
  33. package/dist/src/database/manager.d.ts +35 -0
  34. package/dist/src/database/types.d.ts +20 -0
  35. package/dist/src/index.d.ts +45 -0
  36. package/dist/src/index.js +15 -1
  37. package/dist/src/middleware/compression.d.ts +6 -0
  38. package/dist/src/middleware/cors.d.ts +11 -0
  39. package/dist/src/middleware/presets.d.ts +13 -0
  40. package/dist/src/middleware/rate-limit.d.ts +32 -0
  41. package/dist/src/middleware/security.d.ts +22 -0
  42. package/dist/src/middleware/static.d.ts +11 -0
  43. package/dist/src/openapi/generator.d.ts +19 -0
  44. package/dist/src/router/radix-router.d.ts +18 -0
  45. package/dist/src/router/radix-tree.d.ts +16 -0
  46. package/dist/src/router/router.d.ts +33 -0
  47. package/dist/src/testing/index.d.ts +25 -0
  48. package/dist/src/utils/cookies.d.ts +3 -0
  49. package/dist/src/utils/logger.d.ts +12 -0
  50. package/dist/src/utils/signals.d.ts +6 -0
  51. package/dist/src/utils/sse.d.ts +6 -0
  52. package/dist/src/validation/index.d.ts +3 -0
  53. package/dist/src/validation/simple.d.ts +5 -0
  54. package/dist/src/validation/types.d.ts +32 -0
  55. package/dist/src/validation/zod.d.ts +4 -0
  56. package/dist/src/views/index.d.ts +1 -0
  57. package/dist/src/views/types.d.ts +3 -0
  58. package/dist/tests/adapters.test.d.ts +1 -0
  59. package/dist/tests/batch.test.d.ts +1 -0
  60. package/dist/tests/body-parser.test.d.ts +1 -0
  61. package/dist/tests/compression-sse.test.d.ts +1 -0
  62. package/dist/tests/cookies.test.d.ts +1 -0
  63. package/dist/tests/cors.test.d.ts +1 -0
  64. package/dist/tests/database.test.d.ts +1 -0
  65. package/dist/tests/dx.test.d.ts +1 -0
  66. package/dist/tests/dx.test.js +100 -50
  67. package/dist/tests/ecosystem.test.d.ts +1 -0
  68. package/dist/tests/features.test.d.ts +1 -0
  69. package/dist/tests/fusion.test.d.ts +1 -0
  70. package/dist/tests/http-basic.test.d.ts +1 -0
  71. package/dist/tests/logger.test.d.ts +1 -0
  72. package/dist/tests/middleware.test.d.ts +1 -0
  73. package/dist/tests/observability.test.d.ts +1 -0
  74. package/dist/tests/openapi.test.d.ts +1 -0
  75. package/dist/tests/plugin.test.d.ts +1 -0
  76. package/dist/tests/plugins.test.d.ts +1 -0
  77. package/dist/tests/rate-limit.test.d.ts +1 -0
  78. package/dist/tests/resources.test.d.ts +1 -0
  79. package/dist/tests/scheduler.test.d.ts +1 -0
  80. package/dist/tests/schema-routes.test.d.ts +1 -0
  81. package/dist/tests/security.test.d.ts +1 -0
  82. package/dist/tests/server-db.test.d.ts +1 -0
  83. package/dist/tests/smoke.test.d.ts +1 -0
  84. package/dist/tests/sqlite-fusion.test.d.ts +1 -0
  85. package/dist/tests/static.test.d.ts +1 -0
  86. package/dist/tests/stream.test.d.ts +1 -0
  87. package/dist/tests/task-metrics.test.d.ts +1 -0
  88. package/dist/tests/tasks.test.d.ts +1 -0
  89. package/dist/tests/testing.test.d.ts +1 -0
  90. package/dist/tests/validation.test.d.ts +1 -0
  91. package/dist/tests/websocket.test.d.ts +1 -0
  92. package/dist/vitest.config.d.ts +2 -0
  93. package/package.json +39 -6
  94. package/src/core/server.ts +130 -91
  95. package/src/core/types.ts +14 -4
  96. package/src/index.ts +16 -0
  97. package/tests/dx.test.ts +109 -57
  98. package/tsconfig.json +1 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Buffer pool for response bodies (small, medium, large).
3
+ * Reuses allocated buffers to reduce GC pressure.
4
+ */
5
+ export type BufferPoolConfig = {
6
+ smallSize?: number;
7
+ mediumSize?: number;
8
+ largeSize?: number;
9
+ smallPoolSize?: number;
10
+ mediumPoolSize?: number;
11
+ largePoolSize?: number;
12
+ };
13
+ export declare class BufferPool {
14
+ private readonly smallSize;
15
+ private readonly mediumSize;
16
+ private readonly largeSize;
17
+ private readonly smallBuffers;
18
+ private readonly mediumBuffers;
19
+ private readonly largeBuffers;
20
+ private readonly smallPoolSize;
21
+ private readonly mediumPoolSize;
22
+ private readonly largePoolSize;
23
+ constructor(config?: BufferPoolConfig);
24
+ /**
25
+ * Acquire a buffer suitable for the given size.
26
+ * Returns a buffer from the appropriate pool.
27
+ */
28
+ acquire(size: number): Buffer;
29
+ /**
30
+ * Release a buffer back to the appropriate pool.
31
+ */
32
+ release(buffer: Buffer): void;
33
+ /**
34
+ * Get the number of available buffers in each pool.
35
+ */
36
+ getPoolStatus(): {
37
+ small: number;
38
+ medium: number;
39
+ large: number;
40
+ };
41
+ }
@@ -0,0 +1,7 @@
1
+ import type { QHTTPXOptions } from './types';
2
+ export type LoadConfigOptions = {
3
+ env?: NodeJS.ProcessEnv;
4
+ defaults?: QHTTPXOptions;
5
+ prefix?: string;
6
+ };
7
+ export declare function loadConfig(options?: LoadConfigOptions): QHTTPXOptions;
@@ -0,0 +1,14 @@
1
+ import type { QHTTPXContext } from './types';
2
+ export interface RequestFusionOptions {
3
+ windowMs?: number;
4
+ vary?: string[];
5
+ }
6
+ export declare class RequestFusion {
7
+ private inflight;
8
+ private cache;
9
+ private options;
10
+ constructor(options?: boolean | RequestFusionOptions);
11
+ coalesce(ctx: QHTTPXContext, next: (ctx: QHTTPXContext) => Promise<void> | void): Promise<void>;
12
+ private applyResult;
13
+ private getKey;
14
+ }
@@ -0,0 +1,22 @@
1
+ import pino from 'pino';
2
+ export type LogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
3
+ export interface LoggerOptions {
4
+ level?: LogLevel;
5
+ pretty?: boolean;
6
+ name?: string;
7
+ }
8
+ export declare class Logger {
9
+ private pino;
10
+ constructor(options?: LoggerOptions);
11
+ info(msg: string, ...args: any[]): void;
12
+ info(obj: object, msg?: string, ...args: any[]): void;
13
+ error(msg: string, ...args: any[]): void;
14
+ error(obj: object, msg?: string, ...args: any[]): void;
15
+ warn(msg: string, ...args: any[]): void;
16
+ warn(obj: object, msg?: string, ...args: any[]): void;
17
+ debug(msg: string, ...args: any[]): void;
18
+ debug(obj: object, msg?: string, ...args: any[]): void;
19
+ fatal(msg: string, ...args: any[]): void;
20
+ fatal(obj: object, msg?: string, ...args: any[]): void;
21
+ child(bindings: pino.Bindings): Logger;
22
+ }
@@ -0,0 +1,45 @@
1
+ import type { TaskEngine, TaskMetrics } from './tasks';
2
+ import { Scheduler } from './scheduler';
3
+ export type LatencySnapshot = {
4
+ p50: number | null;
5
+ p95: number | null;
6
+ p99: number | null;
7
+ };
8
+ export type MetricsSnapshot = {
9
+ totalRequests: number;
10
+ inFlightRequests: number;
11
+ totalErrors: number;
12
+ totalTimeouts: number;
13
+ requestsPerSecond: number;
14
+ latency: LatencySnapshot;
15
+ scheduler: {
16
+ inFlight: number;
17
+ };
18
+ tasks?: TaskMetrics;
19
+ memory: {
20
+ rssBytes: number;
21
+ heapUsedBytes: number;
22
+ };
23
+ };
24
+ export declare class Metrics {
25
+ private readonly scheduler;
26
+ private readonly taskEngine?;
27
+ private readonly latencies;
28
+ private readonly maxLatencies;
29
+ private readonly enabled;
30
+ private totalRequests;
31
+ private inFlightRequests;
32
+ private totalErrors;
33
+ private totalTimeouts;
34
+ constructor(scheduler: Scheduler, options?: {
35
+ maxLatencies?: number;
36
+ enabled?: boolean;
37
+ }, taskEngine?: TaskEngine);
38
+ onRequestStart(): void;
39
+ onRequestEnd(durationMs: number, statusCode: number): void;
40
+ onTimeout(): void;
41
+ snapshot(): MetricsSnapshot;
42
+ private recordLatency;
43
+ private latencySnapshot;
44
+ private percentile;
45
+ }
@@ -0,0 +1,9 @@
1
+ export type WorkerSetting = 'auto' | number;
2
+ export declare function calculateWorkerCount(setting: WorkerSetting): number;
3
+ export type ResourceThresholds = {
4
+ maxRssBytes?: number;
5
+ };
6
+ export type ResourceSample = {
7
+ rssBytes: number;
8
+ };
9
+ export declare function isResourceOverloaded(sample: ResourceSample, thresholds: ResourceThresholds): boolean;
@@ -0,0 +1,34 @@
1
+ import { RoutePriority } from './types';
2
+ export type SchedulerOptions = {
3
+ maxConcurrency?: number;
4
+ workers?: number;
5
+ };
6
+ export type RunOptions = {
7
+ priority?: RoutePriority;
8
+ onOverloaded?: () => void;
9
+ timeoutMs?: number;
10
+ onTimeout?: () => void;
11
+ };
12
+ export type SchedulerStats = {
13
+ inFlight: number;
14
+ maxConcurrency: number;
15
+ workers: number;
16
+ perWorkerStats: {
17
+ workerId: number;
18
+ queued: number;
19
+ }[];
20
+ };
21
+ export declare class Scheduler {
22
+ private inFlight;
23
+ private readonly maxConcurrency;
24
+ private readonly workerCount;
25
+ private readonly perWorkerQueues;
26
+ private nextWorkerIndex;
27
+ constructor(options?: SchedulerOptions);
28
+ getCurrentInFlight(): number;
29
+ /**
30
+ * Get scheduler statistics (queued tasks per worker, etc.)
31
+ */
32
+ getStats(): SchedulerStats;
33
+ run(task: () => void | Promise<void>, options: RunOptions): Promise<void>;
34
+ }
@@ -0,0 +1,26 @@
1
+ import { QHTTPX } from './server';
2
+ import { QHTTPXHandler, QHTTPXRouteOptions, QHTTPXMiddleware, QHTTPXPlugin, QHTTPXPluginOptions } from './types';
3
+ /**
4
+ * A Scope represents a prefixed or isolated context for plugins.
5
+ * It proxies methods to the main QHTTPX instance but handles prefixing.
6
+ */
7
+ export declare class QHTTPXScope {
8
+ private readonly app;
9
+ private readonly prefix;
10
+ constructor(app: QHTTPX, prefix?: string);
11
+ /**
12
+ * Registers a sub-plugin within this scope.
13
+ * Prefixes are concatenated (e.g. /v1 + /users = /v1/users).
14
+ */
15
+ register<Options extends QHTTPXPluginOptions>(plugin: QHTTPXPlugin<Options>, options?: Options): Promise<void>;
16
+ use(middleware: QHTTPXMiddleware): void;
17
+ get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
18
+ post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
19
+ put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
20
+ delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
21
+ patch(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
22
+ options(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
23
+ head(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
24
+ getApp(): QHTTPX;
25
+ private joinPaths;
26
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Fast JSON serializer using fast-json-stringify
3
+ * For best performance, use schema-based stringifiers per route
4
+ */
5
+ export declare function fastJsonStringify(value: unknown, schema?: unknown): string;
6
+ /**
7
+ * Get a pre-compiled stringifier for a specific schema
8
+ * Use this in route handlers for maximum performance
9
+ */
10
+ export declare function getStringifier(schema: unknown): (value: unknown) => string;
@@ -0,0 +1,86 @@
1
+ import http from 'http';
2
+ import { WebSocketManager } from './websocket';
3
+ import { OpenAPIOptions } from '../openapi/generator';
4
+ import { HTTPMethod, QHTTPXErrorHandler, QHTTPXHandler, QHTTPXMethodNotAllowedHandler, QHTTPXMiddleware, QHTTPXNotFoundHandler, QHTTPXOptions, QHTTPXRouteOptions, QHTTPXPlugin, QHTTPXPluginOptions } from './types';
5
+ import { Logger } from './logger';
6
+ export declare class QHTTPX {
7
+ private readonly options;
8
+ private readonly server;
9
+ readonly logger: Logger;
10
+ private readonly router;
11
+ private readonly scheduler;
12
+ private readonly middlewares;
13
+ private readonly workerCount;
14
+ private readonly tasks;
15
+ private readonly metrics;
16
+ private readonly contextPool;
17
+ private readonly bufferPool;
18
+ private pipelineRunner;
19
+ private errorHandler?;
20
+ private notFoundHandler?;
21
+ private methodNotAllowedHandler?;
22
+ private readonly tracer?;
23
+ private readonly onStartHooks;
24
+ private readonly onBeforeShutdownHooks;
25
+ private readonly onShutdownHooks;
26
+ private nextRequestId;
27
+ private readonly wsManager;
28
+ private readonly ultraMode;
29
+ private readonly batchExecutor?;
30
+ private readonly fusion?;
31
+ private readonly validator;
32
+ constructor(options?: QHTTPXOptions);
33
+ get serverInstance(): http.Server;
34
+ get websocket(): WebSocketManager;
35
+ setErrorHandler(handler: QHTTPXErrorHandler): void;
36
+ setNotFoundHandler(handler: QHTTPXNotFoundHandler): void;
37
+ setMethodNotAllowedHandler(handler: QHTTPXMethodNotAllowedHandler): void;
38
+ set404Handler(handler: QHTTPXNotFoundHandler): void;
39
+ set405Handler(handler: QHTTPXMethodNotAllowedHandler): void;
40
+ /**
41
+ * Alias for setErrorHandler
42
+ */
43
+ onError(handler: QHTTPXErrorHandler): void;
44
+ /**
45
+ * Alias for setNotFoundHandler
46
+ */
47
+ notFound(handler: QHTTPXNotFoundHandler): void;
48
+ onStart(hook: () => void | Promise<void>): void;
49
+ onBeforeShutdown(hook: () => void | Promise<void>): void;
50
+ onShutdown(hook: () => void | Promise<void>): void;
51
+ upgrade(path: string, handler: import('./websocket').WSHandler): void;
52
+ use(middleware: QHTTPXMiddleware): void;
53
+ private compileMiddlewarePipeline;
54
+ private compileRoutePipeline;
55
+ _registerRoute(method: HTTPMethod, path: string, handlerOrOptions: QHTTPXHandler | QHTTPXRouteOptions): void;
56
+ register<Options extends QHTTPXPluginOptions>(plugin: QHTTPXPlugin<Options>, options?: Options): Promise<void>;
57
+ private registerRoute;
58
+ get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
59
+ post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
60
+ put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
61
+ delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void;
62
+ route(path: string): {
63
+ get(handler: QHTTPXHandler | QHTTPXRouteOptions): /*elided*/ any;
64
+ post(handler: QHTTPXHandler | QHTTPXRouteOptions): /*elided*/ any;
65
+ put(handler: QHTTPXHandler | QHTTPXRouteOptions): /*elided*/ any;
66
+ delete(handler: QHTTPXHandler | QHTTPXRouteOptions): /*elided*/ any;
67
+ };
68
+ task(name: string, handler: import('./types').QHTTPXTaskHandler, options?: import('./types').QHTTPXTaskOptions): void;
69
+ enqueue(name: string, payload: unknown): Promise<void>;
70
+ op(name: string, handler: import('./types').QHTTPXOpHandler): void;
71
+ private registerInternalRoutes;
72
+ getOpenAPI(options: OpenAPIOptions): object;
73
+ listen(port: number, hostname?: string): Promise<{
74
+ port: number;
75
+ }>;
76
+ close(): Promise<void>;
77
+ shutdown(): Promise<void>;
78
+ private createContext;
79
+ private acquireContext;
80
+ private releaseContext;
81
+ private handleRequest;
82
+ private handleUpgrade;
83
+ private runLifecycleHooks;
84
+ private handleError;
85
+ private generateRequestId;
86
+ }
@@ -111,6 +111,18 @@ class QHTTPX {
111
111
  set405Handler(handler) {
112
112
  this.setMethodNotAllowedHandler(handler);
113
113
  }
114
+ /**
115
+ * Alias for setErrorHandler
116
+ */
117
+ onError(handler) {
118
+ this.setErrorHandler(handler);
119
+ }
120
+ /**
121
+ * Alias for setNotFoundHandler
122
+ */
123
+ notFound(handler) {
124
+ this.setNotFoundHandler(handler);
125
+ }
114
126
  onStart(hook) {
115
127
  this.onStartHooks.push(hook);
116
128
  }
@@ -239,12 +251,16 @@ class QHTTPX {
239
251
  const middleware = middlewares[i];
240
252
  const next = pipeline;
241
253
  pipeline = (ctx) => {
242
- return middleware(ctx, async () => {
254
+ const nextFn = async () => {
243
255
  const result = next(ctx);
244
256
  if (result && typeof result.then === 'function') {
245
257
  await result;
246
258
  }
247
- });
259
+ };
260
+ // Attach next to ctx for destructuring support
261
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
262
+ ctx.next = nextFn;
263
+ return middleware(ctx, nextFn);
248
264
  };
249
265
  }
250
266
  if (stringifier) {
@@ -450,95 +466,98 @@ class QHTTPX {
450
466
  requestId: '',
451
467
  requestStart: 0,
452
468
  serializer: null,
453
- json(payload, status = 200) {
454
- const res = this.res;
455
- if (!res.headersSent) {
456
- res.statusCode = status;
457
- res.setHeader('content-type', 'application/json; charset=utf-8');
458
- }
459
- let body;
460
- if (this.serializer) {
461
- body = this.serializer(payload);
462
- }
463
- else if (useFastStringify) {
464
- body = (0, serializer_1.fastJsonStringify)(payload);
465
- }
466
- else if (jsonSerializer) {
467
- body = jsonSerializer(payload);
468
- }
469
- else {
470
- body = JSON.stringify(payload);
471
- }
472
- res.end(body);
473
- },
474
- send(payload, status = 200) {
475
- const res = this.res;
476
- if (!res.headersSent) {
477
- res.statusCode = status;
478
- }
479
- res.end(payload);
480
- },
481
- html(payload, status = 200) {
482
- const res = this.res;
483
- if (!res.headersSent) {
484
- res.statusCode = status;
485
- res.setHeader('content-type', 'text/html; charset=utf-8');
486
- }
487
- res.end(payload);
488
- },
489
- redirect(url, status = 302) {
490
- const res = this.res;
491
- if (!res.headersSent) {
492
- res.statusCode = status;
493
- res.setHeader('Location', url);
494
- }
495
- res.end();
496
- },
497
- setCookie(name, value, options) {
498
- const res = this.res;
499
- const serialized = (0, cookies_1.serializeCookie)(name, value, options);
500
- let existing = res.getHeader('Set-Cookie');
501
- if (Array.isArray(existing)) {
502
- existing.push(serialized);
503
- res.setHeader('Set-Cookie', existing);
504
- }
505
- else if (existing) {
506
- res.setHeader('Set-Cookie', [existing, serialized]);
507
- }
508
- else {
509
- res.setHeader('Set-Cookie', serialized);
510
- }
511
- },
469
+ path: '',
470
+ error: undefined,
512
471
  db: this.options.database,
513
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
514
- render: async (view, locals) => {
515
- const engine = this.options.viewEngine;
516
- if (!engine) {
517
- throw new Error('No view engine registered');
518
- }
519
- const viewsPath = this.options.viewsPath || process.cwd();
520
- const fullPath = path_1.default.resolve(viewsPath, view);
521
- const html = await engine.render(fullPath, locals || {});
522
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
523
- const res = ctx.res;
524
- if (!res.headersSent) {
525
- res.statusCode = 200;
526
- res.setHeader('content-type', 'text/html; charset=utf-8');
527
- }
528
- res.end(html);
529
- },
530
- validate: async (schema, data) => {
531
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
532
- const target = data ?? ctx.body;
533
- const result = await this.validator.validate(schema, target);
534
- if (result.success) {
535
- return result.data;
536
- }
537
- throw new types_1.HttpError(400, 'Validation Error', {
538
- code: 'VALIDATION_ERROR',
539
- details: result.error,
540
- });
541
- },
472
+ };
473
+ // Helper to get response object from closure-captured ctx
474
+ // We use arrow functions to ensure they don't depend on 'this' context at call site
475
+ // enabling destructuring like: ({ json }) => json(...)
476
+ ctx.json = (payload, status = 200) => {
477
+ const res = ctx.res;
478
+ if (!res.headersSent) {
479
+ res.statusCode = status;
480
+ res.setHeader('content-type', 'application/json; charset=utf-8');
481
+ }
482
+ let body;
483
+ if (ctx.serializer) {
484
+ body = ctx.serializer(payload);
485
+ }
486
+ else if (useFastStringify) {
487
+ body = (0, serializer_1.fastJsonStringify)(payload);
488
+ }
489
+ else if (jsonSerializer) {
490
+ body = jsonSerializer(payload);
491
+ }
492
+ else {
493
+ body = JSON.stringify(payload);
494
+ }
495
+ res.end(body);
496
+ };
497
+ ctx.send = (payload, status = 200) => {
498
+ const res = ctx.res;
499
+ if (!res.headersSent) {
500
+ res.statusCode = status;
501
+ }
502
+ res.end(payload);
503
+ };
504
+ ctx.html = (payload, status = 200) => {
505
+ const res = ctx.res;
506
+ if (!res.headersSent) {
507
+ res.statusCode = status;
508
+ res.setHeader('content-type', 'text/html; charset=utf-8');
509
+ }
510
+ res.end(payload);
511
+ };
512
+ ctx.redirect = (url, status = 302) => {
513
+ const res = ctx.res;
514
+ if (!res.headersSent) {
515
+ res.statusCode = status;
516
+ res.setHeader('Location', url);
517
+ }
518
+ res.end();
519
+ };
520
+ ctx.setCookie = (name, value, options) => {
521
+ const res = ctx.res;
522
+ const serialized = (0, cookies_1.serializeCookie)(name, value, options);
523
+ let existing = res.getHeader('Set-Cookie');
524
+ if (Array.isArray(existing)) {
525
+ existing.push(serialized);
526
+ res.setHeader('Set-Cookie', existing);
527
+ }
528
+ else if (existing) {
529
+ res.setHeader('Set-Cookie', [existing, serialized]);
530
+ }
531
+ else {
532
+ res.setHeader('Set-Cookie', serialized);
533
+ }
534
+ };
535
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
536
+ ctx.render = async (view, locals) => {
537
+ const engine = this.options.viewEngine;
538
+ if (!engine) {
539
+ throw new Error('No view engine registered');
540
+ }
541
+ const viewsPath = this.options.viewsPath || process.cwd();
542
+ const fullPath = path_1.default.resolve(viewsPath, view);
543
+ const html = await engine.render(fullPath, locals || {});
544
+ const res = ctx.res;
545
+ if (!res.headersSent) {
546
+ res.statusCode = 200;
547
+ res.setHeader('content-type', 'text/html; charset=utf-8');
548
+ }
549
+ res.end(html);
550
+ };
551
+ ctx.validate = async (schema, data) => {
552
+ const target = data ?? ctx.body;
553
+ const result = await this.validator.validate(schema, target);
554
+ if (result.success) {
555
+ return result.data;
556
+ }
557
+ throw new types_1.HttpError(400, 'Validation Error', {
558
+ code: 'VALIDATION_ERROR',
559
+ details: result.error,
560
+ });
542
561
  };
543
562
  return ctx;
544
563
  }
@@ -565,6 +584,7 @@ class QHTTPX {
565
584
  }
566
585
  mutableCtx.state = {};
567
586
  mutableCtx.disableAutoEnd = false;
587
+ mutableCtx.path = url.pathname;
568
588
  return ctx;
569
589
  }
570
590
  releaseContext(ctx) {
@@ -582,6 +602,8 @@ class QHTTPX {
582
602
  mutableCtx.serializer = null;
583
603
  mutableCtx.cookies = null;
584
604
  mutableCtx.state = null;
605
+ mutableCtx.path = '';
606
+ mutableCtx.error = undefined;
585
607
  // render method is static per context instance creation (closure over options),
586
608
  // but good to keep it consistent.
587
609
  // Wait, 'render' is defined in 'createContext' and depends on 'this.options'.
@@ -851,23 +873,29 @@ class QHTTPX {
851
873
  }
852
874
  }
853
875
  }
854
- async handleError(err, ctx) {
876
+ handleError(err, ctx) {
855
877
  const res = ctx.res;
856
878
  if (res.writableEnded) {
857
879
  return;
858
880
  }
859
881
  if (this.errorHandler) {
860
882
  try {
861
- const result = this.errorHandler(err, ctx);
883
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
884
+ const errorContext = ctx;
885
+ errorContext.error = err;
886
+ const result = this.errorHandler(errorContext);
862
887
  if (result && typeof result.then === 'function') {
863
- await result;
888
+ return result.then(() => {
889
+ // Ensure response is sent if handler didn't
890
+ });
864
891
  }
865
892
  if (res.writableEnded) {
866
893
  return;
867
894
  }
868
895
  }
869
- catch {
896
+ catch (handlerErr) {
870
897
  // Fall through to default error handling below
898
+ console.error('Error in error handler:', handlerErr);
871
899
  }
872
900
  }
873
901
  if (res.writableEnded) {
@@ -0,0 +1,15 @@
1
+ import { Readable } from 'stream';
2
+ import { QHTTPXContext } from './types';
3
+ export type SseOptions = {
4
+ retryMs?: number;
5
+ };
6
+ export type SseStream = {
7
+ send: (data: unknown, event?: string) => void;
8
+ close: () => void;
9
+ };
10
+ export declare function createSseStream(ctx: QHTTPXContext, options?: SseOptions): SseStream;
11
+ export type StreamOptions = {
12
+ contentType?: string;
13
+ status?: number;
14
+ };
15
+ export declare function sendStream(ctx: QHTTPXContext, stream: Readable, options?: StreamOptions): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { Scheduler } from './scheduler';
2
+ export type QHTTPXTaskHandler = (payload: unknown) => void | Promise<void>;
3
+ export type QHTTPXTaskOptions = {
4
+ maxRetries?: number;
5
+ backoffMs?: number;
6
+ };
7
+ export type TaskMetrics = {
8
+ registeredTasks: number;
9
+ totalEnqueued: number;
10
+ totalCompleted: number;
11
+ totalFailed: number;
12
+ totalOverloaded: number;
13
+ totalRetried: number;
14
+ };
15
+ export declare class TaskEngine {
16
+ private readonly scheduler;
17
+ private readonly tasks;
18
+ private registeredTasksCount;
19
+ private totalEnqueued;
20
+ private totalCompleted;
21
+ private totalFailed;
22
+ private totalOverloaded;
23
+ private totalRetried;
24
+ constructor(scheduler: Scheduler);
25
+ register(name: string, handler: QHTTPXTaskHandler, options?: QHTTPXTaskOptions): void;
26
+ enqueue(name: string, payload: unknown): Promise<void>;
27
+ getMetrics(): TaskMetrics;
28
+ private executeWithRetry;
29
+ }