qhttpx 1.9.1 → 1.9.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 +22 -0
- package/README.md +79 -17
- package/dist/examples/api-server.d.ts +1 -0
- package/dist/examples/api-server.js +77 -0
- package/dist/examples/basic.d.ts +1 -0
- package/dist/examples/basic.js +10 -0
- package/dist/examples/compression.d.ts +1 -0
- package/dist/examples/compression.js +17 -0
- package/dist/examples/cors.d.ts +1 -0
- package/dist/examples/cors.js +19 -0
- package/dist/examples/errors.d.ts +1 -0
- package/dist/examples/errors.js +25 -0
- package/dist/examples/file-upload.d.ts +1 -0
- package/dist/examples/file-upload.js +24 -0
- package/dist/examples/fusion.d.ts +1 -0
- package/dist/examples/fusion.js +21 -0
- package/dist/examples/rate-limiting.d.ts +1 -0
- package/dist/examples/rate-limiting.js +17 -0
- package/dist/examples/validation.d.ts +1 -0
- package/dist/examples/validation.js +23 -0
- package/dist/examples/websockets.d.ts +1 -0
- package/dist/examples/websockets.js +20 -0
- package/dist/package.json +112 -0
- package/dist/src/benchmarks/compare-frameworks.js +119 -0
- package/dist/src/benchmarks/compare.d.ts +1 -0
- package/dist/src/benchmarks/compare.js +288 -0
- package/dist/src/benchmarks/quantam-users.d.ts +1 -0
- package/dist/src/benchmarks/quantam-users.js +56 -0
- package/dist/src/benchmarks/simple-json.d.ts +1 -0
- package/dist/src/benchmarks/simple-json.js +60 -0
- package/dist/src/benchmarks/ultra-mode.d.ts +1 -0
- package/dist/src/benchmarks/ultra-mode.js +94 -0
- package/dist/src/buffer-pool.js +70 -0
- package/dist/src/cli/index.d.ts +2 -0
- package/dist/src/cli/index.js +222 -0
- package/dist/src/client/index.d.ts +17 -0
- package/dist/src/client/index.js +72 -0
- package/dist/src/config.js +50 -0
- package/dist/src/cookies.js +59 -0
- package/dist/src/core/batch.d.ts +24 -0
- package/dist/src/core/batch.js +97 -0
- package/dist/src/core/body-parser.d.ts +15 -0
- package/dist/src/core/body-parser.js +121 -0
- package/dist/src/core/buffer-pool.d.ts +41 -0
- package/dist/src/core/buffer-pool.js +70 -0
- package/dist/src/core/config.d.ts +7 -0
- package/dist/src/core/config.js +50 -0
- package/dist/src/core/errors.d.ts +34 -0
- package/dist/src/core/errors.js +70 -0
- package/dist/src/core/fusion.d.ts +20 -0
- package/dist/src/core/fusion.js +193 -0
- package/dist/src/core/logger.d.ts +22 -0
- package/dist/src/core/logger.js +49 -0
- package/dist/src/core/metrics.d.ts +48 -0
- package/dist/src/core/metrics.js +117 -0
- package/dist/src/core/native-adapter.d.ts +11 -0
- package/dist/src/core/native-adapter.js +211 -0
- package/dist/src/core/resources.d.ts +9 -0
- package/dist/src/core/resources.js +25 -0
- package/dist/src/core/scheduler.d.ts +34 -0
- package/dist/src/core/scheduler.js +85 -0
- package/dist/src/core/scope.d.ts +26 -0
- package/dist/src/core/scope.js +68 -0
- package/dist/src/core/serializer.d.ts +10 -0
- package/dist/src/core/serializer.js +44 -0
- package/dist/src/core/server.d.ts +138 -0
- package/dist/src/core/server.js +1082 -0
- package/dist/src/core/stream.d.ts +15 -0
- package/dist/src/core/stream.js +71 -0
- package/dist/src/core/tasks.d.ts +29 -0
- package/dist/src/core/tasks.js +87 -0
- package/dist/src/core/types.d.ts +173 -0
- package/dist/src/core/types.js +19 -0
- package/dist/src/core/websocket.d.ts +25 -0
- package/dist/src/core/websocket.js +86 -0
- package/dist/src/core/worker-queue.d.ts +41 -0
- package/dist/src/core/worker-queue.js +73 -0
- package/dist/src/cors.js +66 -0
- package/dist/src/database/adapters/memory.d.ts +21 -0
- package/dist/src/database/adapters/memory.js +90 -0
- package/dist/src/database/adapters/mongo.d.ts +11 -0
- package/dist/src/database/adapters/mongo.js +141 -0
- package/dist/src/database/adapters/postgres.d.ts +10 -0
- package/dist/src/database/adapters/postgres.js +111 -0
- package/dist/src/database/adapters/sqlite.d.ts +10 -0
- package/dist/src/database/adapters/sqlite.js +42 -0
- package/dist/src/database/coalescer.d.ts +14 -0
- package/dist/src/database/coalescer.js +134 -0
- package/dist/src/database/manager.d.ts +35 -0
- package/dist/src/database/manager.js +87 -0
- package/dist/src/database/types.d.ts +20 -0
- package/dist/src/database/types.js +2 -0
- package/dist/src/index.d.ts +50 -0
- package/dist/src/index.js +91 -0
- package/dist/src/logger.js +45 -0
- package/dist/src/metrics.js +111 -0
- package/dist/src/middleware/compression.d.ts +2 -0
- package/dist/src/middleware/compression.js +133 -0
- package/dist/src/middleware/cors.d.ts +2 -0
- package/dist/src/middleware/cors.js +66 -0
- package/dist/src/middleware/presets.d.ts +16 -0
- package/dist/src/middleware/presets.js +52 -0
- package/dist/src/middleware/rate-limit.d.ts +14 -0
- package/dist/src/middleware/rate-limit.js +83 -0
- package/dist/src/middleware/security.d.ts +21 -0
- package/dist/src/middleware/security.js +69 -0
- package/dist/src/middleware/static.d.ts +11 -0
- package/dist/src/middleware/static.js +191 -0
- package/dist/src/native/index.d.ts +32 -0
- package/dist/src/native/index.js +141 -0
- package/dist/src/openapi/generator.d.ts +19 -0
- package/dist/src/openapi/generator.js +149 -0
- package/dist/src/presets.js +33 -0
- package/dist/src/radix-router.js +89 -0
- package/dist/src/radix-tree.js +81 -0
- package/dist/src/resources.js +25 -0
- package/dist/src/router/radix-router.d.ts +18 -0
- package/dist/src/router/radix-router.js +89 -0
- package/dist/src/router/radix-tree.d.ts +18 -0
- package/dist/src/router/radix-tree.js +131 -0
- package/dist/src/router/router.d.ts +34 -0
- package/dist/src/router/router.js +186 -0
- package/dist/src/router.js +138 -0
- package/dist/src/scheduler.js +85 -0
- package/dist/src/security.js +69 -0
- package/dist/src/server.js +685 -0
- package/dist/src/signals.js +31 -0
- package/dist/src/static.js +107 -0
- package/dist/src/stream.js +71 -0
- package/dist/src/tasks.js +87 -0
- package/dist/src/testing/index.d.ts +25 -0
- package/dist/src/testing/index.js +84 -0
- package/dist/src/testing.js +40 -0
- package/dist/src/types.js +19 -0
- package/dist/src/utils/cookies.d.ts +3 -0
- package/dist/src/utils/cookies.js +59 -0
- package/dist/src/utils/logger.d.ts +12 -0
- package/dist/src/utils/logger.js +45 -0
- package/dist/src/utils/signals.d.ts +6 -0
- package/dist/src/utils/signals.js +31 -0
- package/dist/src/utils/sse.d.ts +6 -0
- package/dist/src/utils/sse.js +32 -0
- package/dist/src/utils/testing.js +40 -0
- package/dist/src/validation/index.d.ts +3 -0
- package/dist/src/validation/index.js +19 -0
- package/dist/src/validation/simple.d.ts +5 -0
- package/dist/src/validation/simple.js +102 -0
- package/dist/src/validation/types.d.ts +32 -0
- package/dist/src/validation/types.js +12 -0
- package/dist/src/validation/zod.d.ts +4 -0
- package/dist/src/validation/zod.js +18 -0
- package/dist/src/views/index.d.ts +1 -0
- package/dist/src/views/index.js +17 -0
- package/dist/src/views/types.d.ts +3 -0
- package/dist/src/views/types.js +2 -0
- package/dist/src/worker-queue.js +73 -0
- package/dist/tests/adapters.test.d.ts +1 -0
- package/dist/tests/adapters.test.js +106 -0
- package/dist/tests/batch.test.d.ts +1 -0
- package/dist/tests/batch.test.js +117 -0
- package/dist/tests/body-parser.test.d.ts +1 -0
- package/dist/tests/body-parser.test.js +52 -0
- package/dist/tests/compression-sse.test.d.ts +1 -0
- package/dist/tests/compression-sse.test.js +87 -0
- package/dist/tests/cookies.test.d.ts +1 -0
- package/dist/tests/cookies.test.js +63 -0
- package/dist/tests/cors.test.d.ts +1 -0
- package/dist/tests/cors.test.js +55 -0
- package/dist/tests/database.test.d.ts +1 -0
- package/dist/tests/database.test.js +80 -0
- package/dist/tests/dx.test.d.ts +1 -0
- package/dist/tests/dx.test.js +114 -0
- package/dist/tests/ecosystem.test.d.ts +1 -0
- package/dist/tests/ecosystem.test.js +133 -0
- package/dist/tests/features.test.d.ts +1 -0
- package/dist/tests/features.test.js +47 -0
- package/dist/tests/fusion.test.d.ts +1 -0
- package/dist/tests/fusion.test.js +92 -0
- package/dist/tests/http-basic.test.d.ts +1 -0
- package/dist/tests/http-basic.test.js +124 -0
- package/dist/tests/logger.test.d.ts +1 -0
- package/dist/tests/logger.test.js +33 -0
- package/dist/tests/middleware.test.d.ts +1 -0
- package/dist/tests/middleware.test.js +109 -0
- package/dist/tests/native-adapter.test.d.ts +1 -0
- package/dist/tests/native-adapter.test.js +71 -0
- package/dist/tests/observability.test.d.ts +1 -0
- package/dist/tests/observability.test.js +59 -0
- package/dist/tests/openapi.test.d.ts +1 -0
- package/dist/tests/openapi.test.js +64 -0
- package/dist/tests/plugin.test.d.ts +1 -0
- package/dist/tests/plugin.test.js +65 -0
- package/dist/tests/plugins.test.d.ts +1 -0
- package/dist/tests/plugins.test.js +71 -0
- package/dist/tests/rate-limit.test.d.ts +1 -0
- package/dist/tests/rate-limit.test.js +77 -0
- package/dist/tests/resources.test.d.ts +1 -0
- package/dist/tests/resources.test.js +47 -0
- package/dist/tests/scheduler.test.d.ts +1 -0
- package/dist/tests/scheduler.test.js +46 -0
- package/dist/tests/schema-routes.test.d.ts +1 -0
- package/dist/tests/schema-routes.test.js +77 -0
- package/dist/tests/security.test.d.ts +1 -0
- package/dist/tests/security.test.js +83 -0
- package/dist/tests/server-db.test.d.ts +1 -0
- package/dist/tests/server-db.test.js +72 -0
- package/dist/tests/smoke.test.d.ts +1 -0
- package/dist/tests/smoke.test.js +10 -0
- package/dist/tests/sqlite-fusion.test.d.ts +1 -0
- package/dist/tests/sqlite-fusion.test.js +92 -0
- package/dist/tests/static.test.d.ts +1 -0
- package/dist/tests/static.test.js +102 -0
- package/dist/tests/stream.test.d.ts +1 -0
- package/dist/tests/stream.test.js +44 -0
- package/dist/tests/task-metrics.test.d.ts +1 -0
- package/dist/tests/task-metrics.test.js +53 -0
- package/dist/tests/tasks.test.d.ts +1 -0
- package/dist/tests/tasks.test.js +62 -0
- package/dist/tests/testing.test.d.ts +1 -0
- package/dist/tests/testing.test.js +47 -0
- package/dist/tests/validation.test.d.ts +1 -0
- package/dist/tests/validation.test.js +107 -0
- package/dist/tests/websocket.test.d.ts +1 -0
- package/dist/tests/websocket.test.js +146 -0
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +9 -0
- package/docs/FUSION.md +19 -0
- package/package.json +4 -15
- package/prebuilds/darwin-arm64/qhttpx.node +0 -0
- package/prebuilds/linux-x64/qhttpx.node +0 -0
- package/prebuilds/win32-x64/qhttpx.node +0 -0
- package/scripts/install-native.js +26 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachSignalHandlers = attachSignalHandlers;
|
|
4
|
+
function attachSignalHandlers(app, options = {}) {
|
|
5
|
+
const signals = options.signals ?? ['SIGINT', 'SIGTERM'];
|
|
6
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
7
|
+
let shuttingDown = false;
|
|
8
|
+
const handler = async (signal) => {
|
|
9
|
+
if (shuttingDown) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
shuttingDown = true;
|
|
13
|
+
console.log(`Received ${signal}, shutting down...`);
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
console.error('Shutdown timed out, forcing exit. (some connections might be lost)');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
try {
|
|
19
|
+
await app.shutdown();
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error('Error during shutdown:', err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
for (const signal of signals) {
|
|
29
|
+
process.on(signal, handler);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createStaticMiddleware = createStaticMiddleware;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
function guessContentType(filePath) {
|
|
10
|
+
const ext = path_1.default.extname(filePath).toLowerCase();
|
|
11
|
+
if (ext === '.html' || ext === '.htm') {
|
|
12
|
+
return 'text/html; charset=utf-8';
|
|
13
|
+
}
|
|
14
|
+
if (ext === '.js') {
|
|
15
|
+
return 'application/javascript; charset=utf-8';
|
|
16
|
+
}
|
|
17
|
+
if (ext === '.css') {
|
|
18
|
+
return 'text/css; charset=utf-8';
|
|
19
|
+
}
|
|
20
|
+
if (ext === '.json') {
|
|
21
|
+
return 'application/json; charset=utf-8';
|
|
22
|
+
}
|
|
23
|
+
if (ext === '.png') {
|
|
24
|
+
return 'image/png';
|
|
25
|
+
}
|
|
26
|
+
if (ext === '.jpg' || ext === '.jpeg') {
|
|
27
|
+
return 'image/jpeg';
|
|
28
|
+
}
|
|
29
|
+
if (ext === '.gif') {
|
|
30
|
+
return 'image/gif';
|
|
31
|
+
}
|
|
32
|
+
if (ext === '.svg') {
|
|
33
|
+
return 'image/svg+xml';
|
|
34
|
+
}
|
|
35
|
+
return 'application/octet-stream';
|
|
36
|
+
}
|
|
37
|
+
function createStaticMiddleware(options) {
|
|
38
|
+
const root = path_1.default.resolve(options.root);
|
|
39
|
+
const indexFile = options.index ?? 'index.html';
|
|
40
|
+
const fallthrough = options.fallthrough ?? false;
|
|
41
|
+
return async (ctx, next) => {
|
|
42
|
+
const method = ctx.req.method || 'GET';
|
|
43
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
44
|
+
if (fallthrough) {
|
|
45
|
+
await next();
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
ctx.res.statusCode = 405;
|
|
49
|
+
ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
50
|
+
ctx.res.end('Method Not Allowed');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let requestPath = ctx.url.pathname || '/';
|
|
54
|
+
requestPath = requestPath.replace(/^[/\\]+/, '');
|
|
55
|
+
if (requestPath === '') {
|
|
56
|
+
requestPath = indexFile;
|
|
57
|
+
}
|
|
58
|
+
else if (requestPath.endsWith('/')) {
|
|
59
|
+
requestPath = requestPath + indexFile;
|
|
60
|
+
}
|
|
61
|
+
let safePath = path_1.default.normalize(requestPath);
|
|
62
|
+
safePath = safePath.replace(/^(\.\.(\/|\\|$))+/, '');
|
|
63
|
+
const filePath = path_1.default.join(root, safePath);
|
|
64
|
+
let stat;
|
|
65
|
+
try {
|
|
66
|
+
stat = await fs_1.default.promises.stat(filePath);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
if (fallthrough) {
|
|
70
|
+
await next();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
ctx.res.statusCode = 404;
|
|
74
|
+
ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
75
|
+
ctx.res.end('Not Found');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (!stat.isFile()) {
|
|
79
|
+
if (fallthrough) {
|
|
80
|
+
await next();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
ctx.res.statusCode = 404;
|
|
84
|
+
ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
85
|
+
ctx.res.end('Not Found');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const contentType = guessContentType(filePath);
|
|
89
|
+
ctx.res.setHeader('content-type', contentType);
|
|
90
|
+
ctx.res.statusCode = 200;
|
|
91
|
+
if (method === 'HEAD') {
|
|
92
|
+
ctx.res.end();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const data = await fs_1.default.promises.readFile(filePath);
|
|
97
|
+
ctx.res.end(data);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
if (!ctx.res.headersSent) {
|
|
101
|
+
ctx.res.statusCode = 500;
|
|
102
|
+
ctx.res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
103
|
+
}
|
|
104
|
+
ctx.res.end('Internal Server Error');
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSseStream = createSseStream;
|
|
4
|
+
exports.sendStream = sendStream;
|
|
5
|
+
function createSseStream(ctx, options = {}) {
|
|
6
|
+
const res = ctx.res;
|
|
7
|
+
if (!res.headersSent) {
|
|
8
|
+
res.statusCode = 200;
|
|
9
|
+
res.setHeader('content-type', 'text/event-stream; charset=utf-8');
|
|
10
|
+
res.setHeader('cache-control', 'no-cache');
|
|
11
|
+
res.setHeader('connection', 'keep-alive');
|
|
12
|
+
}
|
|
13
|
+
const anyRes = res;
|
|
14
|
+
if (anyRes.flushHeaders) {
|
|
15
|
+
anyRes.flushHeaders();
|
|
16
|
+
}
|
|
17
|
+
if (typeof options.retryMs === 'number') {
|
|
18
|
+
res.write(`retry: ${options.retryMs}\n\n`);
|
|
19
|
+
}
|
|
20
|
+
const send = (data, event) => {
|
|
21
|
+
if (res.writableEnded) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
25
|
+
let chunk = '';
|
|
26
|
+
if (event) {
|
|
27
|
+
chunk += `event: ${event}\n`;
|
|
28
|
+
}
|
|
29
|
+
chunk += `data: ${payload}\n\n`;
|
|
30
|
+
res.write(chunk);
|
|
31
|
+
if (anyRes.flush) {
|
|
32
|
+
anyRes.flush();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const close = () => {
|
|
36
|
+
if (!res.writableEnded) {
|
|
37
|
+
res.end();
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
return { send, close };
|
|
41
|
+
}
|
|
42
|
+
function sendStream(ctx, stream, options = {}) {
|
|
43
|
+
const res = ctx.res;
|
|
44
|
+
if (!res.headersSent) {
|
|
45
|
+
if (options.status !== undefined) {
|
|
46
|
+
res.statusCode = options.status;
|
|
47
|
+
}
|
|
48
|
+
if (options.contentType) {
|
|
49
|
+
res.setHeader('content-type', options.contentType);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return new Promise((resolve, reject) => {
|
|
53
|
+
stream.on('error', (err) => {
|
|
54
|
+
if (!res.headersSent) {
|
|
55
|
+
res.statusCode = 500;
|
|
56
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
57
|
+
}
|
|
58
|
+
if (!res.writableEnded) {
|
|
59
|
+
res.end('Internal Server Error');
|
|
60
|
+
}
|
|
61
|
+
reject(err);
|
|
62
|
+
});
|
|
63
|
+
stream.on('end', () => {
|
|
64
|
+
if (!res.writableEnded) {
|
|
65
|
+
res.end();
|
|
66
|
+
}
|
|
67
|
+
resolve();
|
|
68
|
+
});
|
|
69
|
+
stream.pipe(res, { end: false });
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TaskEngine = void 0;
|
|
4
|
+
class TaskEngine {
|
|
5
|
+
constructor(scheduler) {
|
|
6
|
+
this.tasks = new Map();
|
|
7
|
+
this.registeredTasksCount = 0;
|
|
8
|
+
this.totalEnqueued = 0;
|
|
9
|
+
this.totalCompleted = 0;
|
|
10
|
+
this.totalFailed = 0;
|
|
11
|
+
this.totalOverloaded = 0;
|
|
12
|
+
this.totalRetried = 0;
|
|
13
|
+
this.scheduler = scheduler;
|
|
14
|
+
}
|
|
15
|
+
register(name, handler, options = {}) {
|
|
16
|
+
if (!this.tasks.has(name)) {
|
|
17
|
+
this.registeredTasksCount += 1;
|
|
18
|
+
}
|
|
19
|
+
this.tasks.set(name, {
|
|
20
|
+
name,
|
|
21
|
+
handler,
|
|
22
|
+
options,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async enqueue(name, payload) {
|
|
26
|
+
const def = this.tasks.get(name);
|
|
27
|
+
if (!def) {
|
|
28
|
+
throw new Error(`Task "${name}" is not registered`);
|
|
29
|
+
}
|
|
30
|
+
this.totalEnqueued += 1;
|
|
31
|
+
await this.executeWithRetry(def, payload);
|
|
32
|
+
}
|
|
33
|
+
getMetrics() {
|
|
34
|
+
return {
|
|
35
|
+
registeredTasks: this.registeredTasksCount,
|
|
36
|
+
totalEnqueued: this.totalEnqueued,
|
|
37
|
+
totalCompleted: this.totalCompleted,
|
|
38
|
+
totalFailed: this.totalFailed,
|
|
39
|
+
totalOverloaded: this.totalOverloaded,
|
|
40
|
+
totalRetried: this.totalRetried,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async executeWithRetry(def, payload) {
|
|
44
|
+
const maxRetries = def.options.maxRetries ?? 0;
|
|
45
|
+
const backoffMs = def.options.backoffMs ?? 0;
|
|
46
|
+
let attempt = 0;
|
|
47
|
+
for (;;) {
|
|
48
|
+
let overloaded = false;
|
|
49
|
+
let error;
|
|
50
|
+
await this.scheduler.run(async () => {
|
|
51
|
+
try {
|
|
52
|
+
await def.handler(payload);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
error = err;
|
|
56
|
+
}
|
|
57
|
+
}, {
|
|
58
|
+
onOverloaded: () => {
|
|
59
|
+
overloaded = true;
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
if (!overloaded && !error) {
|
|
63
|
+
this.totalCompleted += 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (attempt >= maxRetries) {
|
|
67
|
+
if (error) {
|
|
68
|
+
this.totalFailed += 1;
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
if (overloaded) {
|
|
72
|
+
this.totalOverloaded += 1;
|
|
73
|
+
throw new Error(`Task "${def.name}" overloaded`);
|
|
74
|
+
}
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
attempt += 1;
|
|
78
|
+
this.totalRetried += 1;
|
|
79
|
+
if (backoffMs > 0) {
|
|
80
|
+
await new Promise((resolve) => {
|
|
81
|
+
setTimeout(resolve, backoffMs);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.TaskEngine = TaskEngine;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { QHTTPX } from '../core/server';
|
|
2
|
+
import { HTTPMethod } from '../core/types';
|
|
3
|
+
export declare class TestClient {
|
|
4
|
+
private app;
|
|
5
|
+
private server;
|
|
6
|
+
private baseURL;
|
|
7
|
+
constructor(app: QHTTPX);
|
|
8
|
+
/**
|
|
9
|
+
* Starts the server on a random port.
|
|
10
|
+
* Automatically called by request methods if not started.
|
|
11
|
+
*/
|
|
12
|
+
start(): Promise<void>;
|
|
13
|
+
stop(): Promise<void>;
|
|
14
|
+
request(method: HTTPMethod, path: string, options?: {
|
|
15
|
+
headers?: Record<string, string>;
|
|
16
|
+
body?: any;
|
|
17
|
+
query?: Record<string, string | number | boolean>;
|
|
18
|
+
}): Promise<Response>;
|
|
19
|
+
get(path: string, options?: Omit<Parameters<TestClient['request']>[2], 'body'>): Promise<Response>;
|
|
20
|
+
post(path: string, body?: any, options?: Omit<Parameters<TestClient['request']>[2], 'body'>): Promise<Response>;
|
|
21
|
+
put(path: string, body?: any, options?: Omit<Parameters<TestClient['request']>[2], 'body'>): Promise<Response>;
|
|
22
|
+
delete(path: string, options?: Omit<Parameters<TestClient['request']>[2], 'body'>): Promise<Response>;
|
|
23
|
+
patch(path: string, body?: any, options?: Omit<Parameters<TestClient['request']>[2], 'body'>): Promise<Response>;
|
|
24
|
+
}
|
|
25
|
+
export declare function createTestClient(app: QHTTPX): TestClient;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TestClient = void 0;
|
|
4
|
+
exports.createTestClient = createTestClient;
|
|
5
|
+
class TestClient {
|
|
6
|
+
constructor(app) {
|
|
7
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
8
|
+
this.server = null;
|
|
9
|
+
this.baseURL = '';
|
|
10
|
+
this.app = app;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Starts the server on a random port.
|
|
14
|
+
* Automatically called by request methods if not started.
|
|
15
|
+
*/
|
|
16
|
+
async start() {
|
|
17
|
+
if (this.server)
|
|
18
|
+
return;
|
|
19
|
+
const { port } = await this.app.listen(0);
|
|
20
|
+
this.server = this.app.serverInstance;
|
|
21
|
+
this.baseURL = `http://127.0.0.1:${port}`;
|
|
22
|
+
}
|
|
23
|
+
async stop() {
|
|
24
|
+
if (this.server) {
|
|
25
|
+
await this.app.close();
|
|
26
|
+
this.server = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async request(method, path, options = {}) {
|
|
30
|
+
if (!this.server) {
|
|
31
|
+
await this.start();
|
|
32
|
+
}
|
|
33
|
+
const url = new URL(path, this.baseURL);
|
|
34
|
+
if (options.query) {
|
|
35
|
+
Object.entries(options.query).forEach(([k, v]) => {
|
|
36
|
+
url.searchParams.append(k, String(v));
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
const headers = options.headers || {};
|
|
40
|
+
let bodyPayload;
|
|
41
|
+
if (options.body) {
|
|
42
|
+
if (typeof options.body === 'object' &&
|
|
43
|
+
!(options.body instanceof Uint8Array) &&
|
|
44
|
+
!(options.body instanceof ArrayBuffer) &&
|
|
45
|
+
!(options.body instanceof FormData) &&
|
|
46
|
+
!(options.body instanceof URLSearchParams)) {
|
|
47
|
+
bodyPayload = JSON.stringify(options.body);
|
|
48
|
+
if (!headers['content-type']) {
|
|
49
|
+
headers['content-type'] = 'application/json';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
bodyPayload = options.body;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return fetch(url, {
|
|
57
|
+
method,
|
|
58
|
+
headers,
|
|
59
|
+
body: bodyPayload,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
get(path, options) {
|
|
63
|
+
return this.request('GET', path, options);
|
|
64
|
+
}
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
|
+
post(path, body, options) {
|
|
67
|
+
return this.request('POST', path, { ...options, body });
|
|
68
|
+
}
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
put(path, body, options) {
|
|
71
|
+
return this.request('PUT', path, { ...options, body });
|
|
72
|
+
}
|
|
73
|
+
delete(path, options) {
|
|
74
|
+
return this.request('DELETE', path, options);
|
|
75
|
+
}
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
patch(path, body, options) {
|
|
78
|
+
return this.request('PATCH', path, { ...options, body });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
exports.TestClient = TestClient;
|
|
82
|
+
function createTestClient(app) {
|
|
83
|
+
return new TestClient(app);
|
|
84
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createTestClient = createTestClient;
|
|
4
|
+
async function createTestClient(app) {
|
|
5
|
+
const { port } = await app.listen(0, '127.0.0.1');
|
|
6
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
7
|
+
const request = async (method, path, body, extraHeaders = {}) => {
|
|
8
|
+
const headers = { ...extraHeaders };
|
|
9
|
+
const init = {
|
|
10
|
+
method,
|
|
11
|
+
headers,
|
|
12
|
+
};
|
|
13
|
+
if (body !== undefined) {
|
|
14
|
+
if (typeof body === 'string') {
|
|
15
|
+
init.body = body;
|
|
16
|
+
}
|
|
17
|
+
else if (body instanceof Uint8Array || Buffer.isBuffer(body)) {
|
|
18
|
+
init.body = body;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
init.body = JSON.stringify(body);
|
|
22
|
+
if (!('content-type' in headers)) {
|
|
23
|
+
headers['content-type'] = 'application/json';
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return fetch(`${baseUrl}${path}`, init);
|
|
28
|
+
};
|
|
29
|
+
return {
|
|
30
|
+
get: (path, headers) => request('GET', path, undefined, headers),
|
|
31
|
+
post: (path, body, headers) => request('POST', path, body, headers),
|
|
32
|
+
put: (path, body, headers) => request('PUT', path, body, headers),
|
|
33
|
+
delete: (path, headers) => request('DELETE', path, undefined, headers),
|
|
34
|
+
close: async () => {
|
|
35
|
+
await app.close();
|
|
36
|
+
},
|
|
37
|
+
app,
|
|
38
|
+
url: baseUrl,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HttpError = exports.RoutePriority = void 0;
|
|
4
|
+
var RoutePriority;
|
|
5
|
+
(function (RoutePriority) {
|
|
6
|
+
RoutePriority["CRITICAL"] = "critical";
|
|
7
|
+
RoutePriority["STANDARD"] = "standard";
|
|
8
|
+
RoutePriority["BEST_EFFORT"] = "best-effort";
|
|
9
|
+
})(RoutePriority || (exports.RoutePriority = RoutePriority = {}));
|
|
10
|
+
class HttpError extends Error {
|
|
11
|
+
constructor(status, message, options = {}) {
|
|
12
|
+
super(message ?? 'HTTP Error');
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.code = options.code;
|
|
15
|
+
this.details = options.details;
|
|
16
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.HttpError = HttpError;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseCookies = parseCookies;
|
|
4
|
+
exports.serializeCookie = serializeCookie;
|
|
5
|
+
function parseCookies(header) {
|
|
6
|
+
const list = {};
|
|
7
|
+
if (!header) {
|
|
8
|
+
return list;
|
|
9
|
+
}
|
|
10
|
+
header.split(';').forEach((cookie) => {
|
|
11
|
+
const parts = cookie.split('=');
|
|
12
|
+
const name = parts.shift()?.trim();
|
|
13
|
+
if (name) {
|
|
14
|
+
const value = parts.join('=');
|
|
15
|
+
list[name] = decodeURIComponent(value);
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
return list;
|
|
19
|
+
}
|
|
20
|
+
function serializeCookie(name, value, options = {}) {
|
|
21
|
+
let str = `${name}=${encodeURIComponent(value)}`;
|
|
22
|
+
if (options.maxAge) {
|
|
23
|
+
str += `; Max-Age=${Math.floor(options.maxAge)}`;
|
|
24
|
+
}
|
|
25
|
+
if (options.domain) {
|
|
26
|
+
str += `; Domain=${options.domain}`;
|
|
27
|
+
}
|
|
28
|
+
if (options.path) {
|
|
29
|
+
str += `; Path=${options.path}`;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
str += '; Path=/';
|
|
33
|
+
}
|
|
34
|
+
if (options.expires) {
|
|
35
|
+
str += `; Expires=${options.expires.toUTCString()}`;
|
|
36
|
+
}
|
|
37
|
+
if (options.httpOnly) {
|
|
38
|
+
str += '; HttpOnly';
|
|
39
|
+
}
|
|
40
|
+
if (options.secure) {
|
|
41
|
+
str += '; Secure';
|
|
42
|
+
}
|
|
43
|
+
if (options.sameSite) {
|
|
44
|
+
switch (options.sameSite) {
|
|
45
|
+
case 'lax':
|
|
46
|
+
str += '; SameSite=Lax';
|
|
47
|
+
break;
|
|
48
|
+
case 'strict':
|
|
49
|
+
str += '; SameSite=Strict';
|
|
50
|
+
break;
|
|
51
|
+
case 'none':
|
|
52
|
+
str += '; SameSite=None';
|
|
53
|
+
break;
|
|
54
|
+
default:
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return str;
|
|
59
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { QHTTPXContext, QHTTPXMiddleware } from '../core/types';
|
|
2
|
+
export type LogEntry = {
|
|
3
|
+
method: string;
|
|
4
|
+
path: string;
|
|
5
|
+
status: number;
|
|
6
|
+
durationMs: number;
|
|
7
|
+
requestId?: string;
|
|
8
|
+
};
|
|
9
|
+
export type LoggerOptions = {
|
|
10
|
+
sink?: (entry: LogEntry, ctx: QHTTPXContext) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function createLoggerMiddleware(options?: LoggerOptions): QHTTPXMiddleware;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createLoggerMiddleware = createLoggerMiddleware;
|
|
4
|
+
function createLoggerMiddleware(options = {}) {
|
|
5
|
+
const sink = options.sink ??
|
|
6
|
+
((entry) => {
|
|
7
|
+
const status = entry.status;
|
|
8
|
+
let color = '\x1b[37m';
|
|
9
|
+
if (status >= 200 && status < 300) {
|
|
10
|
+
color = '\x1b[32m';
|
|
11
|
+
}
|
|
12
|
+
else if (status === 404) {
|
|
13
|
+
color = '\x1b[33m';
|
|
14
|
+
}
|
|
15
|
+
else if (status >= 500) {
|
|
16
|
+
color = '\x1b[34m';
|
|
17
|
+
}
|
|
18
|
+
else if (status >= 400) {
|
|
19
|
+
color = '\x1b[35m';
|
|
20
|
+
}
|
|
21
|
+
const reset = '\x1b[0m';
|
|
22
|
+
const prefix = entry.requestId ? `${entry.requestId} ` : '';
|
|
23
|
+
const line = `${prefix}${entry.method} ${entry.path} ${status} ${entry.durationMs}ms`;
|
|
24
|
+
console.log(`${color}${line}${reset}`);
|
|
25
|
+
});
|
|
26
|
+
return async (ctx, next) => {
|
|
27
|
+
const start = ctx.requestStart ?? Date.now();
|
|
28
|
+
try {
|
|
29
|
+
await next();
|
|
30
|
+
}
|
|
31
|
+
finally {
|
|
32
|
+
const durationMs = Math.max(1, Date.now() - start);
|
|
33
|
+
const method = ctx.req.method || 'GET';
|
|
34
|
+
const path = ctx.url.pathname;
|
|
35
|
+
const status = ctx.res.statusCode || 200;
|
|
36
|
+
sink({
|
|
37
|
+
method,
|
|
38
|
+
path,
|
|
39
|
+
status,
|
|
40
|
+
durationMs,
|
|
41
|
+
requestId: ctx.requestId,
|
|
42
|
+
}, ctx);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.attachSignalHandlers = attachSignalHandlers;
|
|
4
|
+
function attachSignalHandlers(app, options = {}) {
|
|
5
|
+
const signals = options.signals ?? ['SIGINT', 'SIGTERM'];
|
|
6
|
+
const timeoutMs = options.timeoutMs ?? 10000;
|
|
7
|
+
let shuttingDown = false;
|
|
8
|
+
const handler = async (signal) => {
|
|
9
|
+
if (shuttingDown) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
shuttingDown = true;
|
|
13
|
+
console.log(`Received ${signal}, shutting down...`);
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
console.error('Shutdown timed out, forcing exit. (some connections might be lost)');
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}, timeoutMs);
|
|
18
|
+
try {
|
|
19
|
+
await app.shutdown();
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
console.error('Error during shutdown:', err);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
for (const signal of signals) {
|
|
29
|
+
process.on(signal, handler);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSSE = createSSE;
|
|
4
|
+
function createSSE(ctx) {
|
|
5
|
+
const res = ctx.res;
|
|
6
|
+
// Disable auto-end so we can keep the connection open
|
|
7
|
+
ctx.disableAutoEnd = true;
|
|
8
|
+
// Only write headers if not already sent
|
|
9
|
+
if (!res.headersSent) {
|
|
10
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
11
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
12
|
+
res.setHeader('Connection', 'keep-alive');
|
|
13
|
+
res.setHeader('X-Accel-Buffering', 'no'); // For Nginx
|
|
14
|
+
// Send initial ping/comment to flush headers and establish connection
|
|
15
|
+
res.write(': connected\n\n');
|
|
16
|
+
}
|
|
17
|
+
const send = (data, event, id) => {
|
|
18
|
+
if (id)
|
|
19
|
+
res.write(`id: ${id}\n`);
|
|
20
|
+
if (event)
|
|
21
|
+
res.write(`event: ${event}\n`);
|
|
22
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
23
|
+
res.write(`data: ${payload}\n\n`);
|
|
24
|
+
};
|
|
25
|
+
const close = () => {
|
|
26
|
+
res.end();
|
|
27
|
+
};
|
|
28
|
+
ctx.req.on('close', () => {
|
|
29
|
+
// console.log('SSE client disconnected');
|
|
30
|
+
});
|
|
31
|
+
return { send, close };
|
|
32
|
+
}
|