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.
- package/.eslintrc.json +22 -0
- package/.github/workflows/ci.yml +32 -0
- package/.github/workflows/npm-publish.yml +37 -0
- package/.github/workflows/release.yml +21 -0
- package/.prettierrc +7 -0
- package/CHANGELOG.md +145 -0
- package/LICENSE +21 -0
- package/README.md +343 -0
- package/dist/package.json +61 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.js +58 -0
- package/dist/src/benchmarks/ultra-mode.js +122 -0
- package/dist/src/cli/index.js +200 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/fusion.js +183 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.js +111 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.js +905 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.js +61 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.js +33 -0
- package/dist/src/middleware/rate-limit.js +77 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.js +81 -0
- package/dist/src/router/router.js +146 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.js +2 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.js +64 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.js +44 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.js +9 -0
- package/docs/AEGIS.md +76 -0
- package/docs/BENCHMARKS.md +36 -0
- package/docs/CAPABILITIES.md +70 -0
- package/docs/CLI.md +43 -0
- package/docs/DATABASE.md +142 -0
- package/docs/ECOSYSTEM.md +146 -0
- package/docs/NEXT_STEPS.md +99 -0
- package/docs/OPENAPI.md +99 -0
- package/docs/PLUGINS.md +59 -0
- package/docs/REAL_WORLD_EXAMPLES.md +109 -0
- package/docs/ROADMAP.md +366 -0
- package/docs/VALIDATION.md +136 -0
- package/eslint.config.cjs +26 -0
- package/examples/api-server.ts +254 -0
- package/package.json +61 -0
- package/src/benchmarks/compare-frameworks.ts +149 -0
- package/src/benchmarks/quantam-users.ts +70 -0
- package/src/benchmarks/simple-json.ts +71 -0
- package/src/benchmarks/ultra-mode.ts +159 -0
- package/src/cli/index.ts +214 -0
- package/src/client/index.ts +93 -0
- package/src/core/batch.ts +110 -0
- package/src/core/body-parser.ts +151 -0
- package/src/core/buffer-pool.ts +96 -0
- package/src/core/config.ts +60 -0
- package/src/core/fusion.ts +210 -0
- package/src/core/logger.ts +70 -0
- package/src/core/metrics.ts +166 -0
- package/src/core/resources.ts +38 -0
- package/src/core/scheduler.ts +126 -0
- package/src/core/scope.ts +87 -0
- package/src/core/serializer.ts +41 -0
- package/src/core/server.ts +1113 -0
- package/src/core/stream.ts +111 -0
- package/src/core/tasks.ts +138 -0
- package/src/core/types.ts +178 -0
- package/src/core/websocket.ts +112 -0
- package/src/core/worker-queue.ts +90 -0
- package/src/database/adapters/memory.ts +99 -0
- package/src/database/adapters/mongo.ts +116 -0
- package/src/database/adapters/postgres.ts +86 -0
- package/src/database/adapters/sqlite.ts +44 -0
- package/src/database/coalescer.ts +153 -0
- package/src/database/manager.ts +97 -0
- package/src/database/types.ts +24 -0
- package/src/index.ts +42 -0
- package/src/middleware/compression.ts +147 -0
- package/src/middleware/cors.ts +98 -0
- package/src/middleware/presets.ts +50 -0
- package/src/middleware/rate-limit.ts +106 -0
- package/src/middleware/security.ts +109 -0
- package/src/middleware/static.ts +216 -0
- package/src/openapi/generator.ts +167 -0
- package/src/router/radix-router.ts +119 -0
- package/src/router/radix-tree.ts +106 -0
- package/src/router/router.ts +190 -0
- package/src/testing/index.ts +104 -0
- package/src/utils/cookies.ts +67 -0
- package/src/utils/logger.ts +59 -0
- package/src/utils/signals.ts +45 -0
- package/src/utils/sse.ts +41 -0
- package/src/validation/index.ts +3 -0
- package/src/validation/simple.ts +93 -0
- package/src/validation/types.ts +38 -0
- package/src/validation/zod.ts +14 -0
- package/src/views/index.ts +1 -0
- package/src/views/types.ts +4 -0
- package/tests/adapters.test.ts +120 -0
- package/tests/batch.test.ts +139 -0
- package/tests/body-parser.test.ts +83 -0
- package/tests/compression-sse.test.ts +98 -0
- package/tests/cookies.test.ts +74 -0
- package/tests/cors.test.ts +79 -0
- package/tests/database.test.ts +90 -0
- package/tests/dx.test.ts +78 -0
- package/tests/ecosystem.test.ts +156 -0
- package/tests/features.test.ts +51 -0
- package/tests/fusion.test.ts +121 -0
- package/tests/http-basic.test.ts +161 -0
- package/tests/logger.test.ts +48 -0
- package/tests/middleware.test.ts +137 -0
- package/tests/observability.test.ts +91 -0
- package/tests/openapi.test.ts +74 -0
- package/tests/plugin.test.ts +85 -0
- package/tests/plugins.test.ts +93 -0
- package/tests/rate-limit.test.ts +97 -0
- package/tests/resources.test.ts +64 -0
- package/tests/scheduler.test.ts +71 -0
- package/tests/schema-routes.test.ts +89 -0
- package/tests/security.test.ts +128 -0
- package/tests/server-db.test.ts +72 -0
- package/tests/smoke.test.ts +9 -0
- package/tests/sqlite-fusion.test.ts +106 -0
- package/tests/static.test.ts +111 -0
- package/tests/stream.test.ts +58 -0
- package/tests/task-metrics.test.ts +78 -0
- package/tests/tasks.test.ts +90 -0
- package/tests/testing.test.ts +53 -0
- package/tests/validation.test.ts +126 -0
- package/tests/websocket.test.ts +132 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { TaskEngine, TaskMetrics } from './tasks';
|
|
2
|
+
import { Scheduler } from './scheduler';
|
|
3
|
+
|
|
4
|
+
export type LatencySnapshot = {
|
|
5
|
+
p50: number | null;
|
|
6
|
+
p95: number | null;
|
|
7
|
+
p99: number | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type MetricsSnapshot = {
|
|
11
|
+
totalRequests: number;
|
|
12
|
+
inFlightRequests: number;
|
|
13
|
+
totalErrors: number;
|
|
14
|
+
totalTimeouts: number;
|
|
15
|
+
requestsPerSecond: number;
|
|
16
|
+
latency: LatencySnapshot;
|
|
17
|
+
scheduler: {
|
|
18
|
+
inFlight: number;
|
|
19
|
+
};
|
|
20
|
+
tasks?: TaskMetrics;
|
|
21
|
+
memory: {
|
|
22
|
+
rssBytes: number;
|
|
23
|
+
heapUsedBytes: number;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export class Metrics {
|
|
28
|
+
private readonly scheduler: Scheduler;
|
|
29
|
+
|
|
30
|
+
private readonly taskEngine?: TaskEngine;
|
|
31
|
+
|
|
32
|
+
private readonly latencies: number[] = [];
|
|
33
|
+
|
|
34
|
+
private readonly maxLatencies: number;
|
|
35
|
+
|
|
36
|
+
private readonly enabled: boolean;
|
|
37
|
+
|
|
38
|
+
private totalRequests = 0;
|
|
39
|
+
|
|
40
|
+
private inFlightRequests = 0;
|
|
41
|
+
|
|
42
|
+
private totalErrors = 0;
|
|
43
|
+
|
|
44
|
+
private totalTimeouts = 0;
|
|
45
|
+
|
|
46
|
+
constructor(
|
|
47
|
+
scheduler: Scheduler,
|
|
48
|
+
options: { maxLatencies?: number; enabled?: boolean } = {},
|
|
49
|
+
taskEngine?: TaskEngine,
|
|
50
|
+
) {
|
|
51
|
+
this.scheduler = scheduler;
|
|
52
|
+
this.taskEngine = taskEngine;
|
|
53
|
+
this.maxLatencies = options.maxLatencies ?? 1000;
|
|
54
|
+
this.enabled = options.enabled ?? true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onRequestStart(): void {
|
|
58
|
+
if (!this.enabled) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
this.inFlightRequests += 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onRequestEnd(durationMs: number, statusCode: number): void {
|
|
65
|
+
if (!this.enabled) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
this.totalRequests += 1;
|
|
69
|
+
if (this.inFlightRequests > 0) {
|
|
70
|
+
this.inFlightRequests -= 1;
|
|
71
|
+
}
|
|
72
|
+
if (statusCode >= 500) {
|
|
73
|
+
this.totalErrors += 1;
|
|
74
|
+
}
|
|
75
|
+
this.recordLatency(durationMs);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
onTimeout(): void {
|
|
79
|
+
if (!this.enabled) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
this.totalTimeouts += 1;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
snapshot(): MetricsSnapshot {
|
|
86
|
+
const uptime = process.uptime();
|
|
87
|
+
const requestsPerSecond =
|
|
88
|
+
uptime > 0 ? this.totalRequests / uptime : this.totalRequests;
|
|
89
|
+
|
|
90
|
+
const latency = this.latencySnapshot();
|
|
91
|
+
|
|
92
|
+
const memoryUsage = process.memoryUsage();
|
|
93
|
+
|
|
94
|
+
let tasks: TaskMetrics | undefined;
|
|
95
|
+
if (this.taskEngine) {
|
|
96
|
+
tasks = this.taskEngine.getMetrics();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
totalRequests: this.totalRequests,
|
|
101
|
+
inFlightRequests: this.inFlightRequests,
|
|
102
|
+
totalErrors: this.totalErrors,
|
|
103
|
+
totalTimeouts: this.totalTimeouts,
|
|
104
|
+
requestsPerSecond,
|
|
105
|
+
latency,
|
|
106
|
+
scheduler: {
|
|
107
|
+
inFlight: this.scheduler.getCurrentInFlight(),
|
|
108
|
+
},
|
|
109
|
+
tasks,
|
|
110
|
+
memory: {
|
|
111
|
+
rssBytes: memoryUsage.rss,
|
|
112
|
+
heapUsedBytes: memoryUsage.heapUsed,
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private recordLatency(durationMs: number): void {
|
|
118
|
+
if (!this.enabled) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!Number.isFinite(durationMs) || durationMs < 0) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
this.latencies.push(durationMs);
|
|
125
|
+
if (this.latencies.length > this.maxLatencies) {
|
|
126
|
+
this.latencies.shift();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private latencySnapshot(): LatencySnapshot {
|
|
131
|
+
if (this.latencies.length === 0) {
|
|
132
|
+
return {
|
|
133
|
+
p50: null,
|
|
134
|
+
p95: null,
|
|
135
|
+
p99: null,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
140
|
+
|
|
141
|
+
const p50 = this.percentile(sorted, 0.5);
|
|
142
|
+
const p95 = this.percentile(sorted, 0.95);
|
|
143
|
+
const p99 = this.percentile(sorted, 0.99);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
p50,
|
|
147
|
+
p95,
|
|
148
|
+
p99,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private percentile(sorted: number[], p: number): number {
|
|
153
|
+
if (sorted.length === 0) {
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
if (p <= 0) {
|
|
157
|
+
return sorted[0];
|
|
158
|
+
}
|
|
159
|
+
if (p >= 1) {
|
|
160
|
+
return sorted[sorted.length - 1];
|
|
161
|
+
}
|
|
162
|
+
const index = Math.floor(p * (sorted.length - 1));
|
|
163
|
+
return sorted[index];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
|
|
3
|
+
export type WorkerSetting = 'auto' | number;
|
|
4
|
+
|
|
5
|
+
export function calculateWorkerCount(setting: WorkerSetting): number {
|
|
6
|
+
if (typeof setting === 'number') {
|
|
7
|
+
if (!Number.isFinite(setting) || setting <= 0) {
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
return Math.floor(setting);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const cpuCount = os.cpus().length || 1;
|
|
14
|
+
return Math.max(1, cpuCount);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type ResourceThresholds = {
|
|
18
|
+
maxRssBytes?: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ResourceSample = {
|
|
22
|
+
rssBytes: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function isResourceOverloaded(
|
|
26
|
+
sample: ResourceSample,
|
|
27
|
+
thresholds: ResourceThresholds,
|
|
28
|
+
): boolean {
|
|
29
|
+
if (
|
|
30
|
+
thresholds.maxRssBytes !== undefined &&
|
|
31
|
+
sample.rssBytes > thresholds.maxRssBytes
|
|
32
|
+
) {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { WorkerQueue } from './worker-queue';
|
|
2
|
+
import { RoutePriority } from './types';
|
|
3
|
+
|
|
4
|
+
export type SchedulerOptions = {
|
|
5
|
+
maxConcurrency?: number;
|
|
6
|
+
workers?: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type RunOptions = {
|
|
10
|
+
priority?: RoutePriority;
|
|
11
|
+
onOverloaded?: () => void;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
onTimeout?: () => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type SchedulerStats = {
|
|
17
|
+
inFlight: number;
|
|
18
|
+
maxConcurrency: number;
|
|
19
|
+
workers: number;
|
|
20
|
+
perWorkerStats: {
|
|
21
|
+
workerId: number;
|
|
22
|
+
queued: number;
|
|
23
|
+
}[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export class Scheduler {
|
|
27
|
+
private inFlight = 0;
|
|
28
|
+
|
|
29
|
+
private readonly maxConcurrency: number;
|
|
30
|
+
|
|
31
|
+
private readonly workerCount: number;
|
|
32
|
+
|
|
33
|
+
private readonly perWorkerQueues: WorkerQueue<() => void>[];
|
|
34
|
+
|
|
35
|
+
private nextWorkerIndex = 0;
|
|
36
|
+
|
|
37
|
+
constructor(options: SchedulerOptions = {}) {
|
|
38
|
+
const max = options.maxConcurrency ?? Infinity;
|
|
39
|
+
this.maxConcurrency = max > 0 ? max : Infinity;
|
|
40
|
+
|
|
41
|
+
// Initialize per-worker queues
|
|
42
|
+
this.workerCount = options.workers ?? 1;
|
|
43
|
+
this.perWorkerQueues = [];
|
|
44
|
+
for (let i = 0; i < this.workerCount; i += 1) {
|
|
45
|
+
this.perWorkerQueues.push(new WorkerQueue(1024));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
getCurrentInFlight(): number {
|
|
50
|
+
return this.inFlight;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get scheduler statistics (queued tasks per worker, etc.)
|
|
55
|
+
*/
|
|
56
|
+
getStats(): SchedulerStats {
|
|
57
|
+
return {
|
|
58
|
+
inFlight: this.inFlight,
|
|
59
|
+
maxConcurrency: this.maxConcurrency,
|
|
60
|
+
workers: this.workerCount,
|
|
61
|
+
perWorkerStats: this.perWorkerQueues.map((q, i) => ({
|
|
62
|
+
workerId: i,
|
|
63
|
+
queued: q.getSize(),
|
|
64
|
+
})),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async run(
|
|
69
|
+
task: () => void | Promise<void>,
|
|
70
|
+
options: RunOptions,
|
|
71
|
+
): Promise<void> {
|
|
72
|
+
const priority = options.priority ?? RoutePriority.STANDARD;
|
|
73
|
+
|
|
74
|
+
let threshold = this.maxConcurrency;
|
|
75
|
+
if (priority === RoutePriority.BEST_EFFORT) {
|
|
76
|
+
// Shed best-effort requests if we are above 80% capacity
|
|
77
|
+
threshold = Math.max(1, Math.floor(this.maxConcurrency * 0.8));
|
|
78
|
+
} else if (priority === RoutePriority.STANDARD) {
|
|
79
|
+
// Shed standard requests if we are above 95% capacity
|
|
80
|
+
threshold = Math.max(1, Math.floor(this.maxConcurrency * 0.95));
|
|
81
|
+
}
|
|
82
|
+
// CRITICAL allows up to 100%
|
|
83
|
+
|
|
84
|
+
if (this.inFlight >= threshold) {
|
|
85
|
+
if (options.onOverloaded) {
|
|
86
|
+
options.onOverloaded();
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.inFlight += 1;
|
|
92
|
+
|
|
93
|
+
let timeoutId: NodeJS.Timeout | undefined;
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (!options.timeoutMs || options.timeoutMs <= 0) {
|
|
97
|
+
const result = task();
|
|
98
|
+
if (result && typeof (result as Promise<void>).then === 'function') {
|
|
99
|
+
await result;
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const taskPromise = Promise.resolve(task()).then(() => 'ok' as const);
|
|
105
|
+
|
|
106
|
+
const timeoutPromise = new Promise<'timeout'>((resolve) => {
|
|
107
|
+
timeoutId = setTimeout(() => {
|
|
108
|
+
resolve('timeout');
|
|
109
|
+
}, options.timeoutMs);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await Promise.race([taskPromise, timeoutPromise]);
|
|
113
|
+
|
|
114
|
+
if (result === 'timeout') {
|
|
115
|
+
if (options.onTimeout) {
|
|
116
|
+
options.onTimeout();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} finally {
|
|
120
|
+
if (timeoutId) {
|
|
121
|
+
clearTimeout(timeoutId);
|
|
122
|
+
}
|
|
123
|
+
this.inFlight -= 1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { QHTTPX } from './server';
|
|
2
|
+
import {
|
|
3
|
+
QHTTPXHandler,
|
|
4
|
+
QHTTPXRouteOptions,
|
|
5
|
+
QHTTPXMiddleware,
|
|
6
|
+
QHTTPXPlugin,
|
|
7
|
+
QHTTPXPluginOptions,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A Scope represents a prefixed or isolated context for plugins.
|
|
12
|
+
* It proxies methods to the main QHTTPX instance but handles prefixing.
|
|
13
|
+
*/
|
|
14
|
+
export class QHTTPXScope {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly app: QHTTPX,
|
|
17
|
+
private readonly prefix: string = ''
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Registers a sub-plugin within this scope.
|
|
22
|
+
* Prefixes are concatenated (e.g. /v1 + /users = /v1/users).
|
|
23
|
+
*/
|
|
24
|
+
public async register<Options extends QHTTPXPluginOptions>(
|
|
25
|
+
plugin: QHTTPXPlugin<Options>,
|
|
26
|
+
options?: Options
|
|
27
|
+
): Promise<void> {
|
|
28
|
+
const newPrefix = this.joinPaths(this.prefix, options?.prefix || '');
|
|
29
|
+
const scope = new QHTTPXScope(this.app, newPrefix);
|
|
30
|
+
await plugin(scope, options as Options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public use(middleware: QHTTPXMiddleware): void {
|
|
34
|
+
// Middleware in scopes is currently global (TODO: Encapsulated middleware)
|
|
35
|
+
this.app.use(middleware);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public get(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
39
|
+
this.app._registerRoute('GET', this.joinPaths(this.prefix, path), handler);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public post(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
43
|
+
this.app._registerRoute('POST', this.joinPaths(this.prefix, path), handler);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public put(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
47
|
+
this.app._registerRoute('PUT', this.joinPaths(this.prefix, path), handler);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public delete(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
51
|
+
this.app._registerRoute('DELETE', this.joinPaths(this.prefix, path), handler);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public patch(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
55
|
+
this.app._registerRoute('PATCH', this.joinPaths(this.prefix, path), handler);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public options(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
59
|
+
this.app._registerRoute('OPTIONS', this.joinPaths(this.prefix, path), handler);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public head(path: string, handler: QHTTPXHandler | QHTTPXRouteOptions): void {
|
|
63
|
+
this.app._registerRoute('HEAD', this.joinPaths(this.prefix, path), handler);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Helper to access the main app if needed
|
|
67
|
+
public getApp(): QHTTPX {
|
|
68
|
+
return this.app;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private joinPaths(head: string, tail: string): string {
|
|
72
|
+
if (!head) return tail;
|
|
73
|
+
if (!tail) return head;
|
|
74
|
+
|
|
75
|
+
// Ensure clean slash joining
|
|
76
|
+
const headSlash = head.endsWith('/');
|
|
77
|
+
const tailSlash = tail.startsWith('/');
|
|
78
|
+
|
|
79
|
+
if (headSlash && tailSlash) {
|
|
80
|
+
return head + tail.slice(1);
|
|
81
|
+
}
|
|
82
|
+
if (!headSlash && !tailSlash) {
|
|
83
|
+
return head + '/' + tail;
|
|
84
|
+
}
|
|
85
|
+
return head + tail;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import stringify from 'fast-json-stringify';
|
|
2
|
+
|
|
3
|
+
// Cache of compiled stringifiers per schema
|
|
4
|
+
const stringifierCache = new Map<string, (value: unknown) => string>();
|
|
5
|
+
|
|
6
|
+
// Default fast JSON stringifier for generic objects
|
|
7
|
+
const defaultStringifier = stringify({
|
|
8
|
+
type: 'object',
|
|
9
|
+
additionalProperties: true,
|
|
10
|
+
} as Parameters<typeof stringify>[0]);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Fast JSON serializer using fast-json-stringify
|
|
14
|
+
* For best performance, use schema-based stringifiers per route
|
|
15
|
+
*/
|
|
16
|
+
export function fastJsonStringify(value: unknown, schema?: unknown): string {
|
|
17
|
+
if (schema) {
|
|
18
|
+
const schemaKey = JSON.stringify(schema);
|
|
19
|
+
let stringifier = stringifierCache.get(schemaKey);
|
|
20
|
+
if (!stringifier) {
|
|
21
|
+
stringifier = stringify(schema as Parameters<typeof stringify>[0]);
|
|
22
|
+
stringifierCache.set(schemaKey, stringifier);
|
|
23
|
+
}
|
|
24
|
+
return stringifier(value);
|
|
25
|
+
}
|
|
26
|
+
return defaultStringifier(value as object);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get a pre-compiled stringifier for a specific schema
|
|
31
|
+
* Use this in route handlers for maximum performance
|
|
32
|
+
*/
|
|
33
|
+
export function getStringifier(schema: unknown) {
|
|
34
|
+
const schemaKey = JSON.stringify(schema);
|
|
35
|
+
let stringifier = stringifierCache.get(schemaKey);
|
|
36
|
+
if (!stringifier) {
|
|
37
|
+
stringifier = stringify(schema as Parameters<typeof stringify>[0]);
|
|
38
|
+
stringifierCache.set(schemaKey, stringifier);
|
|
39
|
+
}
|
|
40
|
+
return stringifier;
|
|
41
|
+
}
|