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,111 @@
|
|
|
1
|
+
import { Readable } from 'stream';
|
|
2
|
+
import { QHTTPXContext } from './types';
|
|
3
|
+
|
|
4
|
+
export type SseOptions = {
|
|
5
|
+
retryMs?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type SseStream = {
|
|
9
|
+
send: (data: unknown, event?: string) => void;
|
|
10
|
+
close: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function createSseStream(
|
|
14
|
+
ctx: QHTTPXContext,
|
|
15
|
+
options: SseOptions = {},
|
|
16
|
+
): SseStream {
|
|
17
|
+
const res = ctx.res;
|
|
18
|
+
|
|
19
|
+
if (!res.headersSent) {
|
|
20
|
+
res.statusCode = 200;
|
|
21
|
+
res.setHeader('content-type', 'text/event-stream; charset=utf-8');
|
|
22
|
+
res.setHeader('cache-control', 'no-cache');
|
|
23
|
+
res.setHeader('connection', 'keep-alive');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const anyRes = res as unknown as {
|
|
27
|
+
flushHeaders?: () => void;
|
|
28
|
+
flush?: () => void;
|
|
29
|
+
};
|
|
30
|
+
if (anyRes.flushHeaders) {
|
|
31
|
+
anyRes.flushHeaders();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (typeof options.retryMs === 'number') {
|
|
35
|
+
res.write(`retry: ${options.retryMs}\n\n`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const send = (data: unknown, event?: string) => {
|
|
39
|
+
if (res.writableEnded) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const payload =
|
|
44
|
+
typeof data === 'string' ? data : JSON.stringify(data);
|
|
45
|
+
|
|
46
|
+
let chunk = '';
|
|
47
|
+
if (event) {
|
|
48
|
+
chunk += `event: ${event}\n`;
|
|
49
|
+
}
|
|
50
|
+
chunk += `data: ${payload}\n\n`;
|
|
51
|
+
|
|
52
|
+
res.write(chunk);
|
|
53
|
+
|
|
54
|
+
if (anyRes.flush) {
|
|
55
|
+
anyRes.flush();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const close = () => {
|
|
60
|
+
if (!res.writableEnded) {
|
|
61
|
+
res.end();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return { send, close };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export type StreamOptions = {
|
|
69
|
+
contentType?: string;
|
|
70
|
+
status?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export function sendStream(
|
|
74
|
+
ctx: QHTTPXContext,
|
|
75
|
+
stream: Readable,
|
|
76
|
+
options: StreamOptions = {},
|
|
77
|
+
): Promise<void> {
|
|
78
|
+
const res = ctx.res;
|
|
79
|
+
|
|
80
|
+
if (!res.headersSent) {
|
|
81
|
+
if (options.status !== undefined) {
|
|
82
|
+
res.statusCode = options.status;
|
|
83
|
+
}
|
|
84
|
+
if (options.contentType) {
|
|
85
|
+
res.setHeader('content-type', options.contentType);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
stream.on('error', (err) => {
|
|
91
|
+
if (!res.headersSent) {
|
|
92
|
+
res.statusCode = 500;
|
|
93
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
94
|
+
}
|
|
95
|
+
if (!res.writableEnded) {
|
|
96
|
+
res.end('Internal Server Error');
|
|
97
|
+
}
|
|
98
|
+
reject(err);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
stream.on('end', () => {
|
|
102
|
+
if (!res.writableEnded) {
|
|
103
|
+
res.end();
|
|
104
|
+
}
|
|
105
|
+
resolve();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
stream.pipe(res, { end: false });
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Scheduler } from './scheduler';
|
|
2
|
+
|
|
3
|
+
export type QHTTPXTaskHandler = (payload: unknown) => void | Promise<void>;
|
|
4
|
+
|
|
5
|
+
export type QHTTPXTaskOptions = {
|
|
6
|
+
maxRetries?: number;
|
|
7
|
+
backoffMs?: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type TaskMetrics = {
|
|
11
|
+
registeredTasks: number;
|
|
12
|
+
totalEnqueued: number;
|
|
13
|
+
totalCompleted: number;
|
|
14
|
+
totalFailed: number;
|
|
15
|
+
totalOverloaded: number;
|
|
16
|
+
totalRetried: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TaskDefinition = {
|
|
20
|
+
name: string;
|
|
21
|
+
handler: QHTTPXTaskHandler;
|
|
22
|
+
options: QHTTPXTaskOptions;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export class TaskEngine {
|
|
26
|
+
private readonly scheduler: Scheduler;
|
|
27
|
+
|
|
28
|
+
private readonly tasks = new Map<string, TaskDefinition>();
|
|
29
|
+
|
|
30
|
+
private registeredTasksCount = 0;
|
|
31
|
+
|
|
32
|
+
private totalEnqueued = 0;
|
|
33
|
+
|
|
34
|
+
private totalCompleted = 0;
|
|
35
|
+
|
|
36
|
+
private totalFailed = 0;
|
|
37
|
+
|
|
38
|
+
private totalOverloaded = 0;
|
|
39
|
+
|
|
40
|
+
private totalRetried = 0;
|
|
41
|
+
|
|
42
|
+
constructor(scheduler: Scheduler) {
|
|
43
|
+
this.scheduler = scheduler;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
register(
|
|
47
|
+
name: string,
|
|
48
|
+
handler: QHTTPXTaskHandler,
|
|
49
|
+
options: QHTTPXTaskOptions = {},
|
|
50
|
+
): void {
|
|
51
|
+
if (!this.tasks.has(name)) {
|
|
52
|
+
this.registeredTasksCount += 1;
|
|
53
|
+
}
|
|
54
|
+
this.tasks.set(name, {
|
|
55
|
+
name,
|
|
56
|
+
handler,
|
|
57
|
+
options,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async enqueue(name: string, payload: unknown): Promise<void> {
|
|
62
|
+
const def = this.tasks.get(name);
|
|
63
|
+
if (!def) {
|
|
64
|
+
throw new Error(`Task "${name}" is not registered`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.totalEnqueued += 1;
|
|
68
|
+
await this.executeWithRetry(def, payload);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getMetrics(): TaskMetrics {
|
|
72
|
+
return {
|
|
73
|
+
registeredTasks: this.registeredTasksCount,
|
|
74
|
+
totalEnqueued: this.totalEnqueued,
|
|
75
|
+
totalCompleted: this.totalCompleted,
|
|
76
|
+
totalFailed: this.totalFailed,
|
|
77
|
+
totalOverloaded: this.totalOverloaded,
|
|
78
|
+
totalRetried: this.totalRetried,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async executeWithRetry(
|
|
83
|
+
def: TaskDefinition,
|
|
84
|
+
payload: unknown,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const maxRetries = def.options.maxRetries ?? 0;
|
|
87
|
+
const backoffMs = def.options.backoffMs ?? 0;
|
|
88
|
+
|
|
89
|
+
let attempt = 0;
|
|
90
|
+
|
|
91
|
+
for (;;) {
|
|
92
|
+
let overloaded = false;
|
|
93
|
+
let error: unknown;
|
|
94
|
+
|
|
95
|
+
await this.scheduler.run(
|
|
96
|
+
async () => {
|
|
97
|
+
try {
|
|
98
|
+
await def.handler(payload);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
error = err;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
onOverloaded: () => {
|
|
105
|
+
overloaded = true;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (!overloaded && !error) {
|
|
111
|
+
this.totalCompleted += 1;
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (attempt >= maxRetries) {
|
|
116
|
+
if (error) {
|
|
117
|
+
this.totalFailed += 1;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (overloaded) {
|
|
121
|
+
this.totalOverloaded += 1;
|
|
122
|
+
throw new Error(`Task "${def.name}" overloaded`);
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
attempt += 1;
|
|
128
|
+
this.totalRetried += 1;
|
|
129
|
+
|
|
130
|
+
if (backoffMs > 0) {
|
|
131
|
+
await new Promise<void>((resolve) => {
|
|
132
|
+
setTimeout(resolve, backoffMs);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'http';
|
|
2
|
+
import { URL } from 'url';
|
|
3
|
+
import type { BufferPool, BufferPoolConfig } from './buffer-pool';
|
|
4
|
+
import type { DatabaseManager } from '../database/manager';
|
|
5
|
+
import type { RouteSchema, Validator } from '../validation/types';
|
|
6
|
+
import type { ViewEngine } from '../views/types';
|
|
7
|
+
import type { RequestFusionOptions } from './fusion';
|
|
8
|
+
|
|
9
|
+
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
10
|
+
|
|
11
|
+
export enum RoutePriority {
|
|
12
|
+
CRITICAL = 'critical',
|
|
13
|
+
STANDARD = 'standard',
|
|
14
|
+
BEST_EFFORT = 'best-effort',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RouteOptions = {
|
|
18
|
+
priority?: RoutePriority;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class HttpError extends Error {
|
|
22
|
+
status: number;
|
|
23
|
+
|
|
24
|
+
code?: string;
|
|
25
|
+
|
|
26
|
+
details?: unknown;
|
|
27
|
+
|
|
28
|
+
constructor(
|
|
29
|
+
status: number,
|
|
30
|
+
message?: string,
|
|
31
|
+
options: { code?: string; details?: unknown } = {},
|
|
32
|
+
) {
|
|
33
|
+
super(message ?? 'HTTP Error');
|
|
34
|
+
this.status = status;
|
|
35
|
+
this.code = options.code;
|
|
36
|
+
this.details = options.details;
|
|
37
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type CookieOptions = {
|
|
42
|
+
domain?: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
expires?: Date;
|
|
45
|
+
maxAge?: number;
|
|
46
|
+
httpOnly?: boolean;
|
|
47
|
+
secure?: boolean;
|
|
48
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export type QHTTPXContext = {
|
|
52
|
+
readonly req: IncomingMessage;
|
|
53
|
+
readonly res: ServerResponse;
|
|
54
|
+
readonly url: URL;
|
|
55
|
+
readonly params: Record<string, string>;
|
|
56
|
+
readonly query: Record<string, string | string[]>;
|
|
57
|
+
body: unknown;
|
|
58
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
59
|
+
files?: Record<string, any>;
|
|
60
|
+
readonly cookies: Record<string, string>;
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
state: Record<string, any>;
|
|
63
|
+
readonly bufferPool: BufferPool;
|
|
64
|
+
requestId: string | undefined;
|
|
65
|
+
requestStart: number | undefined;
|
|
66
|
+
// Serializer for the current route
|
|
67
|
+
serializer?: (value: unknown) => string;
|
|
68
|
+
readonly json: (body: unknown, status?: number) => void;
|
|
69
|
+
readonly send: (body: string | Buffer, status?: number) => void;
|
|
70
|
+
readonly html: (html: string, status?: number) => void;
|
|
71
|
+
readonly redirect: (url: string, status?: number) => void;
|
|
72
|
+
readonly setCookie: (name: string, value: string, options?: CookieOptions) => void;
|
|
73
|
+
readonly db?: DatabaseManager;
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
readonly call?: (op: string, params: any) => Promise<any>;
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
readonly render: (view: string, locals?: Record<string, any>) => Promise<void>;
|
|
78
|
+
readonly validate: <T = unknown>(schema: unknown, data?: unknown) => Promise<T>;
|
|
79
|
+
disableAutoEnd?: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
83
|
+
export type QHTTPXOpHandler = (params: any, ctx: QHTTPXContext) => any | Promise<any>;
|
|
84
|
+
|
|
85
|
+
export type QHTTPXHandler = (ctx: QHTTPXContext) => void | Promise<void>;
|
|
86
|
+
|
|
87
|
+
export type QHTTPXRouteOptions = {
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
89
|
+
schema?: RouteSchema | Record<string, any>;
|
|
90
|
+
handler: QHTTPXHandler;
|
|
91
|
+
priority?: RoutePriority;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type QHTTPXMiddleware = (
|
|
95
|
+
ctx: QHTTPXContext,
|
|
96
|
+
next: () => Promise<void>,
|
|
97
|
+
) => void | Promise<void>;
|
|
98
|
+
|
|
99
|
+
export type QHTTPXErrorHandler = (
|
|
100
|
+
err: unknown,
|
|
101
|
+
ctx: QHTTPXContext,
|
|
102
|
+
) => void | Promise<void>;
|
|
103
|
+
|
|
104
|
+
export type QHTTPXNotFoundHandler = (
|
|
105
|
+
ctx: QHTTPXContext,
|
|
106
|
+
) => void | Promise<void>;
|
|
107
|
+
|
|
108
|
+
export type QHTTPXMethodNotAllowedHandler = (
|
|
109
|
+
ctx: QHTTPXContext,
|
|
110
|
+
allowedMethods: HTTPMethod[],
|
|
111
|
+
) => void | Promise<void>;
|
|
112
|
+
|
|
113
|
+
export type QHTTPXTraceEvent =
|
|
114
|
+
| {
|
|
115
|
+
type: 'request_start';
|
|
116
|
+
method: string;
|
|
117
|
+
path: string;
|
|
118
|
+
requestId?: string;
|
|
119
|
+
}
|
|
120
|
+
| {
|
|
121
|
+
type: 'request_end';
|
|
122
|
+
method: string;
|
|
123
|
+
path: string;
|
|
124
|
+
statusCode: number;
|
|
125
|
+
durationMs: number;
|
|
126
|
+
requestId?: string;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export type QHTTPXTracer = (
|
|
130
|
+
event: QHTTPXTraceEvent,
|
|
131
|
+
) => void | Promise<void>;
|
|
132
|
+
|
|
133
|
+
export type QHTTPXTaskHandler = (payload: unknown) => void | Promise<void>;
|
|
134
|
+
|
|
135
|
+
export type QHTTPXTaskOptions = {
|
|
136
|
+
maxRetries?: number;
|
|
137
|
+
backoffMs?: number;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
export type PerformanceMode = 'balanced' | 'ultra';
|
|
141
|
+
|
|
142
|
+
export type QHTTPXOptions = {
|
|
143
|
+
name?: string;
|
|
144
|
+
workers?: 'auto' | number;
|
|
145
|
+
maxConcurrency?: number;
|
|
146
|
+
requestTimeoutMs?: number;
|
|
147
|
+
maxMemoryBytes?: number;
|
|
148
|
+
maxBodyBytes?: number;
|
|
149
|
+
keepAliveTimeoutMs?: number;
|
|
150
|
+
headersTimeoutMs?: number;
|
|
151
|
+
metricsEnabled?: boolean;
|
|
152
|
+
jsonSerializer?: (value: unknown) => string | Buffer;
|
|
153
|
+
bufferPoolConfig?: BufferPoolConfig;
|
|
154
|
+
errorHandler?: QHTTPXErrorHandler;
|
|
155
|
+
notFoundHandler?: QHTTPXNotFoundHandler;
|
|
156
|
+
methodNotAllowedHandler?: QHTTPXMethodNotAllowedHandler;
|
|
157
|
+
tracer?: QHTTPXTracer;
|
|
158
|
+
performanceMode?: PerformanceMode;
|
|
159
|
+
database?: DatabaseManager;
|
|
160
|
+
enableBatching?: boolean | { endpoint: string };
|
|
161
|
+
validator?: Validator;
|
|
162
|
+
enableRequestFusion?: boolean | RequestFusionOptions;
|
|
163
|
+
viewEngine?: ViewEngine;
|
|
164
|
+
viewsPath?: string;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
168
|
+
export type QHTTPXPlugin<Options = any> = (
|
|
169
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
170
|
+
app: any, // We use 'any' here to avoid circular dependency with QHTTPX class, or we could use an interface
|
|
171
|
+
options: Options
|
|
172
|
+
) => void | Promise<void>;
|
|
173
|
+
|
|
174
|
+
export type QHTTPXPluginOptions = {
|
|
175
|
+
prefix?: string;
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
177
|
+
[key: string]: any;
|
|
178
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
import { URL } from 'url';
|
|
4
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
5
|
+
|
|
6
|
+
export interface QHTTPXWebSocket extends WebSocket {
|
|
7
|
+
join(room: string): void;
|
|
8
|
+
leave(room: string): void;
|
|
9
|
+
id: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type WSHandler = (ws: QHTTPXWebSocket, req: IncomingMessage) => void;
|
|
13
|
+
|
|
14
|
+
export class WebSocketManager {
|
|
15
|
+
private readonly wss: WebSocketServer;
|
|
16
|
+
private readonly handlers: { path: string; handler: WSHandler }[] = [];
|
|
17
|
+
private readonly rooms: Map<string, Set<QHTTPXWebSocket>> = new Map();
|
|
18
|
+
|
|
19
|
+
constructor(private readonly requestIdGenerator: () => string) {
|
|
20
|
+
this.wss = new WebSocketServer({ noServer: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
register(path: string, handler: WSHandler): void {
|
|
24
|
+
this.handlers.push({ path, handler });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async handleUpgrade(
|
|
28
|
+
req: IncomingMessage,
|
|
29
|
+
socket: Duplex,
|
|
30
|
+
head: Buffer,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const rawUrl = req.url || '/';
|
|
33
|
+
const host = req.headers.host || 'localhost';
|
|
34
|
+
const urlObj = new URL(rawUrl, `http://${host}`);
|
|
35
|
+
const path = urlObj.pathname;
|
|
36
|
+
|
|
37
|
+
const handlerEntry = this.handlers.find((entry) => entry.path === path);
|
|
38
|
+
|
|
39
|
+
if (!handlerEntry) {
|
|
40
|
+
socket.destroy();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
45
|
+
const qws = ws as QHTTPXWebSocket;
|
|
46
|
+
qws.id = this.requestIdGenerator();
|
|
47
|
+
|
|
48
|
+
qws.join = (room: string) => this.join(room, qws);
|
|
49
|
+
qws.leave = (room: string) => this.leave(room, qws);
|
|
50
|
+
|
|
51
|
+
qws.on('close', () => {
|
|
52
|
+
this.leaveAll(qws);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
handlerEntry.handler(qws, req);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private join(room: string, ws: QHTTPXWebSocket) {
|
|
60
|
+
if (!this.rooms.has(room)) {
|
|
61
|
+
this.rooms.set(room, new Set());
|
|
62
|
+
}
|
|
63
|
+
this.rooms.get(room)!.add(ws);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private leave(room: string, ws: QHTTPXWebSocket) {
|
|
67
|
+
const set = this.rooms.get(room);
|
|
68
|
+
if (set) {
|
|
69
|
+
set.delete(ws);
|
|
70
|
+
if (set.size === 0) {
|
|
71
|
+
this.rooms.delete(room);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private leaveAll(ws: QHTTPXWebSocket) {
|
|
77
|
+
for (const [room, set] of this.rooms) {
|
|
78
|
+
if (set.has(ws)) {
|
|
79
|
+
set.delete(ws);
|
|
80
|
+
if (set.size === 0) {
|
|
81
|
+
this.rooms.delete(room);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public to(room: string) {
|
|
88
|
+
return {
|
|
89
|
+
emit: (data: unknown) => {
|
|
90
|
+
const set = this.rooms.get(room);
|
|
91
|
+
if (set) {
|
|
92
|
+
const payload =
|
|
93
|
+
typeof data === 'string' ? data : JSON.stringify(data);
|
|
94
|
+
for (const client of set) {
|
|
95
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
96
|
+
client.send(payload);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
public broadcast(data: unknown) {
|
|
105
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
106
|
+
for (const client of this.wss.clients) {
|
|
107
|
+
if (client.readyState === WebSocket.OPEN) {
|
|
108
|
+
client.send(payload);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lock-free (or lock-minimal) work queue for per-worker task distribution.
|
|
3
|
+
* Uses a simple ring buffer for high throughput.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type WorkItem<T> = {
|
|
7
|
+
id: number;
|
|
8
|
+
task: T;
|
|
9
|
+
priority: number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export class WorkerQueue<T> {
|
|
13
|
+
private readonly capacity: number;
|
|
14
|
+
private readonly buffer: (WorkItem<T> | undefined)[];
|
|
15
|
+
|
|
16
|
+
private writeIndex = 0;
|
|
17
|
+
private readIndex = 0;
|
|
18
|
+
private size = 0;
|
|
19
|
+
|
|
20
|
+
constructor(capacity: number = 1024) {
|
|
21
|
+
if (capacity <= 0 || !Number.isInteger(capacity)) {
|
|
22
|
+
throw new Error('Capacity must be a positive integer');
|
|
23
|
+
}
|
|
24
|
+
// Ensure capacity is a power of 2 for efficient modulo with bitmask
|
|
25
|
+
this.capacity = Math.pow(2, Math.ceil(Math.log2(capacity)));
|
|
26
|
+
this.buffer = new Array(this.capacity);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Enqueue a work item. Returns true if successful, false if queue is full.
|
|
31
|
+
*/
|
|
32
|
+
enqueue(item: WorkItem<T>): boolean {
|
|
33
|
+
if (this.size >= this.capacity) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.buffer[this.writeIndex] = item;
|
|
38
|
+
this.writeIndex = (this.writeIndex + 1) & (this.capacity - 1);
|
|
39
|
+
this.size += 1;
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dequeue a work item. Returns undefined if queue is empty.
|
|
46
|
+
*/
|
|
47
|
+
dequeue(): WorkItem<T> | undefined {
|
|
48
|
+
if (this.size <= 0) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const item = this.buffer[this.readIndex];
|
|
53
|
+
this.buffer[this.readIndex] = undefined;
|
|
54
|
+
this.readIndex = (this.readIndex + 1) & (this.capacity - 1);
|
|
55
|
+
this.size -= 1;
|
|
56
|
+
|
|
57
|
+
return item;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Peek at the next item without removing it.
|
|
62
|
+
*/
|
|
63
|
+
peek(): WorkItem<T> | undefined {
|
|
64
|
+
if (this.size <= 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
return this.buffer[this.readIndex];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Check if the queue is empty.
|
|
72
|
+
*/
|
|
73
|
+
isEmpty(): boolean {
|
|
74
|
+
return this.size === 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Get the current size of the queue.
|
|
79
|
+
*/
|
|
80
|
+
getSize(): number {
|
|
81
|
+
return this.size;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get the capacity of the queue.
|
|
86
|
+
*/
|
|
87
|
+
getCapacity(): number {
|
|
88
|
+
return this.capacity;
|
|
89
|
+
}
|
|
90
|
+
}
|