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.
- package/CHANGELOG.md +18 -0
- package/README.md +114 -276
- package/assets/logo.svg +25 -0
- package/dist/package.json +39 -6
- package/dist/src/benchmarks/quantam-users.d.ts +1 -0
- package/dist/src/benchmarks/simple-json.d.ts +1 -0
- package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/client/index.d.ts +17 -0
- package/dist/src/core/batch.d.ts +24 -0
- package/dist/src/core/body-parser.d.ts +15 -0
- package/dist/src/core/buffer-pool.d.ts +41 -0
- package/dist/src/core/config.d.ts +7 -0
- package/dist/src/core/fusion.d.ts +14 -0
- package/dist/src/core/logger.d.ts +22 -0
- package/dist/src/core/metrics.d.ts +45 -0
- package/dist/src/core/resources.d.ts +9 -0
- package/dist/src/core/scheduler.d.ts +34 -0
- package/dist/src/core/scope.d.ts +26 -0
- package/dist/src/core/serializer.d.ts +10 -0
- package/dist/src/core/server.d.ts +86 -0
- package/dist/src/core/server.js +122 -94
- package/dist/src/core/stream.d.ts +15 -0
- package/dist/src/core/tasks.d.ts +29 -0
- package/dist/src/core/types.d.ts +134 -0
- package/dist/src/core/websocket.d.ts +25 -0
- package/dist/src/core/worker-queue.d.ts +41 -0
- package/dist/src/database/adapters/memory.d.ts +21 -0
- package/dist/src/database/adapters/mongo.d.ts +11 -0
- package/dist/src/database/adapters/postgres.d.ts +10 -0
- package/dist/src/database/adapters/sqlite.d.ts +10 -0
- package/dist/src/database/coalescer.d.ts +14 -0
- package/dist/src/database/manager.d.ts +35 -0
- package/dist/src/database/types.d.ts +20 -0
- package/dist/src/index.d.ts +45 -0
- package/dist/src/index.js +15 -1
- package/dist/src/middleware/compression.d.ts +6 -0
- package/dist/src/middleware/cors.d.ts +11 -0
- package/dist/src/middleware/presets.d.ts +13 -0
- package/dist/src/middleware/rate-limit.d.ts +32 -0
- package/dist/src/middleware/security.d.ts +22 -0
- package/dist/src/middleware/static.d.ts +11 -0
- package/dist/src/openapi/generator.d.ts +19 -0
- package/dist/src/router/radix-router.d.ts +18 -0
- package/dist/src/router/radix-tree.d.ts +16 -0
- package/dist/src/router/router.d.ts +33 -0
- package/dist/src/testing/index.d.ts +25 -0
- package/dist/src/utils/cookies.d.ts +3 -0
- package/dist/src/utils/logger.d.ts +12 -0
- package/dist/src/utils/signals.d.ts +6 -0
- package/dist/src/utils/sse.d.ts +6 -0
- package/dist/src/validation/index.d.ts +3 -0
- package/dist/src/validation/simple.d.ts +5 -0
- package/dist/src/validation/types.d.ts +32 -0
- package/dist/src/validation/zod.d.ts +4 -0
- package/dist/src/views/index.d.ts +1 -0
- package/dist/src/views/types.d.ts +3 -0
- package/dist/tests/adapters.test.d.ts +1 -0
- package/dist/tests/batch.test.d.ts +1 -0
- package/dist/tests/body-parser.test.d.ts +1 -0
- package/dist/tests/compression-sse.test.d.ts +1 -0
- package/dist/tests/cookies.test.d.ts +1 -0
- package/dist/tests/cors.test.d.ts +1 -0
- package/dist/tests/database.test.d.ts +1 -0
- package/dist/tests/dx.test.d.ts +1 -0
- package/dist/tests/dx.test.js +100 -50
- package/dist/tests/ecosystem.test.d.ts +1 -0
- package/dist/tests/features.test.d.ts +1 -0
- package/dist/tests/fusion.test.d.ts +1 -0
- package/dist/tests/http-basic.test.d.ts +1 -0
- package/dist/tests/logger.test.d.ts +1 -0
- package/dist/tests/middleware.test.d.ts +1 -0
- package/dist/tests/observability.test.d.ts +1 -0
- package/dist/tests/openapi.test.d.ts +1 -0
- package/dist/tests/plugin.test.d.ts +1 -0
- package/dist/tests/plugins.test.d.ts +1 -0
- package/dist/tests/rate-limit.test.d.ts +1 -0
- package/dist/tests/resources.test.d.ts +1 -0
- package/dist/tests/scheduler.test.d.ts +1 -0
- package/dist/tests/schema-routes.test.d.ts +1 -0
- package/dist/tests/security.test.d.ts +1 -0
- package/dist/tests/server-db.test.d.ts +1 -0
- package/dist/tests/smoke.test.d.ts +1 -0
- package/dist/tests/sqlite-fusion.test.d.ts +1 -0
- package/dist/tests/static.test.d.ts +1 -0
- package/dist/tests/stream.test.d.ts +1 -0
- package/dist/tests/task-metrics.test.d.ts +1 -0
- package/dist/tests/tasks.test.d.ts +1 -0
- package/dist/tests/testing.test.d.ts +1 -0
- package/dist/tests/validation.test.d.ts +1 -0
- package/dist/tests/websocket.test.d.ts +1 -0
- package/dist/vitest.config.d.ts +2 -0
- package/package.json +39 -6
- package/src/core/server.ts +130 -91
- package/src/core/types.ts +14 -4
- package/src/index.ts +16 -0
- package/tests/dx.test.ts +109 -57
- 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,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
|
+
}
|
package/dist/src/core/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|