qhttpx 1.9.2 → 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 +8 -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 +2 -1
- 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,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,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,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
|
+
}
|
|
@@ -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,173 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse, IncomingHttpHeaders } 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
|
+
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
|
|
9
|
+
export declare enum RoutePriority {
|
|
10
|
+
CRITICAL = "critical",
|
|
11
|
+
STANDARD = "standard",
|
|
12
|
+
BEST_EFFORT = "best-effort"
|
|
13
|
+
}
|
|
14
|
+
export type RouteOptions = {
|
|
15
|
+
priority?: RoutePriority;
|
|
16
|
+
};
|
|
17
|
+
export declare class HttpError extends Error {
|
|
18
|
+
status: number;
|
|
19
|
+
code?: string;
|
|
20
|
+
details?: unknown;
|
|
21
|
+
constructor(status: number, message?: string, options?: {
|
|
22
|
+
code?: string;
|
|
23
|
+
details?: unknown;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
export type CookieOptions = {
|
|
27
|
+
domain?: string;
|
|
28
|
+
path?: string;
|
|
29
|
+
expires?: Date;
|
|
30
|
+
maxAge?: number;
|
|
31
|
+
httpOnly?: boolean;
|
|
32
|
+
secure?: boolean;
|
|
33
|
+
sameSite?: 'lax' | 'strict' | 'none';
|
|
34
|
+
};
|
|
35
|
+
export type QHTTPXFile = {
|
|
36
|
+
filename: string;
|
|
37
|
+
encoding: string;
|
|
38
|
+
mimeType: string;
|
|
39
|
+
data: Buffer;
|
|
40
|
+
size: number;
|
|
41
|
+
};
|
|
42
|
+
export type QHTTPXContext = {
|
|
43
|
+
readonly req: IncomingMessage;
|
|
44
|
+
readonly res: ServerResponse;
|
|
45
|
+
readonly headers: IncomingHttpHeaders;
|
|
46
|
+
readonly url: URL;
|
|
47
|
+
readonly method: HTTPMethod;
|
|
48
|
+
readonly ip: string;
|
|
49
|
+
readonly params: Record<string, string>;
|
|
50
|
+
readonly query: Record<string, string | string[]>;
|
|
51
|
+
body: unknown;
|
|
52
|
+
files?: Record<string, QHTTPXFile | QHTTPXFile[]>;
|
|
53
|
+
readonly cookies: Record<string, string>;
|
|
54
|
+
state: Record<string, any>;
|
|
55
|
+
readonly bufferPool: BufferPool;
|
|
56
|
+
requestId: string | undefined;
|
|
57
|
+
requestStart: number | undefined;
|
|
58
|
+
serializer?: (value: unknown) => string;
|
|
59
|
+
readonly json: (body: unknown, status?: number) => void;
|
|
60
|
+
readonly send: (body: string | Buffer, status?: number) => void;
|
|
61
|
+
readonly html: (html: string, status?: number) => void;
|
|
62
|
+
readonly redirect: (url: string, status?: number) => void;
|
|
63
|
+
readonly setCookie: (name: string, value: string, options?: CookieOptions) => void;
|
|
64
|
+
readonly db?: DatabaseManager;
|
|
65
|
+
readonly call?: (op: string, params: any) => Promise<any>;
|
|
66
|
+
readonly render: (view: string, locals?: Record<string, any>) => Promise<void>;
|
|
67
|
+
readonly validate: <T = unknown>(schema: unknown, data?: unknown) => Promise<T>;
|
|
68
|
+
disableAutoEnd?: boolean;
|
|
69
|
+
readonly path: string;
|
|
70
|
+
readonly next?: () => Promise<void>;
|
|
71
|
+
};
|
|
72
|
+
export type QHTTPXOpHandler = (params: any, ctx: QHTTPXContext) => any | Promise<any>;
|
|
73
|
+
export type QHTTPXHandler = (ctx: QHTTPXContext) => void | Promise<void>;
|
|
74
|
+
export type QHTTPXRouteOptions = {
|
|
75
|
+
schema?: RouteSchema | Record<string, any>;
|
|
76
|
+
handler: QHTTPXHandler;
|
|
77
|
+
priority?: RoutePriority;
|
|
78
|
+
};
|
|
79
|
+
export type QHTTPXRouteConfig = Omit<QHTTPXRouteOptions, 'handler'>;
|
|
80
|
+
export type QHTTPXMiddleware = (ctx: QHTTPXContext, next: () => Promise<void>) => void | Promise<void>;
|
|
81
|
+
export type QHTTPXErrorContext = QHTTPXContext & {
|
|
82
|
+
error: unknown;
|
|
83
|
+
};
|
|
84
|
+
export type QHTTPXErrorHandler = (ctx: QHTTPXErrorContext) => void | Promise<void>;
|
|
85
|
+
export type QHTTPXNotFoundHandler = (ctx: QHTTPXContext) => void | Promise<void>;
|
|
86
|
+
export type QHTTPXMethodNotAllowedHandler = (ctx: QHTTPXContext, allowedMethods: HTTPMethod[]) => void | Promise<void>;
|
|
87
|
+
export type QHTTPXTraceEvent = {
|
|
88
|
+
type: 'request_start';
|
|
89
|
+
method: string;
|
|
90
|
+
path: string;
|
|
91
|
+
requestId?: string;
|
|
92
|
+
} | {
|
|
93
|
+
type: 'request_end';
|
|
94
|
+
method: string;
|
|
95
|
+
path: string;
|
|
96
|
+
statusCode: number;
|
|
97
|
+
durationMs: number;
|
|
98
|
+
requestId?: string;
|
|
99
|
+
};
|
|
100
|
+
export type QHTTPXTracer = (event: QHTTPXTraceEvent) => void | Promise<void>;
|
|
101
|
+
export type QHTTPXTaskHandler = (payload: unknown) => void | Promise<void>;
|
|
102
|
+
export type QHTTPXTaskOptions = {
|
|
103
|
+
maxRetries?: number;
|
|
104
|
+
backoffMs?: number;
|
|
105
|
+
};
|
|
106
|
+
export type PerformanceMode = 'balanced' | 'ultra';
|
|
107
|
+
export interface RateLimitStore {
|
|
108
|
+
increment(key: string, windowMs: number): Promise<{
|
|
109
|
+
total: number;
|
|
110
|
+
resetTime: number;
|
|
111
|
+
}>;
|
|
112
|
+
decrement(key: string): Promise<void>;
|
|
113
|
+
reset(key: string): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
export interface RateLimitOptions {
|
|
116
|
+
windowMs?: number;
|
|
117
|
+
max?: number;
|
|
118
|
+
message?: string | object;
|
|
119
|
+
statusCode?: number;
|
|
120
|
+
headers?: boolean;
|
|
121
|
+
keyGenerator?: (ctx: QHTTPXContext) => string;
|
|
122
|
+
skip?: (ctx: QHTTPXContext) => boolean;
|
|
123
|
+
store?: RateLimitStore;
|
|
124
|
+
trustProxy?: boolean;
|
|
125
|
+
}
|
|
126
|
+
export type QHTTPXOptions = {
|
|
127
|
+
name?: string;
|
|
128
|
+
workers?: 'auto' | number;
|
|
129
|
+
maxConcurrency?: number;
|
|
130
|
+
requestTimeoutMs?: number;
|
|
131
|
+
maxMemoryBytes?: number;
|
|
132
|
+
maxBodyBytes?: number;
|
|
133
|
+
keepAliveTimeoutMs?: number;
|
|
134
|
+
headersTimeoutMs?: number;
|
|
135
|
+
metricsEnabled?: boolean;
|
|
136
|
+
jsonSerializer?: (value: unknown) => string | Buffer;
|
|
137
|
+
bufferPoolConfig?: BufferPoolConfig;
|
|
138
|
+
errorHandler?: QHTTPXErrorHandler;
|
|
139
|
+
notFoundHandler?: QHTTPXNotFoundHandler;
|
|
140
|
+
methodNotAllowedHandler?: QHTTPXMethodNotAllowedHandler;
|
|
141
|
+
tracer?: QHTTPXTracer;
|
|
142
|
+
performanceMode?: PerformanceMode;
|
|
143
|
+
database?: DatabaseManager;
|
|
144
|
+
enableBatching?: boolean | {
|
|
145
|
+
endpoint: string;
|
|
146
|
+
};
|
|
147
|
+
validator?: Validator;
|
|
148
|
+
enableRequestFusion?: boolean | RequestFusionOptions;
|
|
149
|
+
viewEngine?: ViewEngine;
|
|
150
|
+
viewsPath?: string;
|
|
151
|
+
rateLimit?: RateLimitOptions;
|
|
152
|
+
cors?: CorsOptions | boolean;
|
|
153
|
+
compression?: CompressionOptions | boolean;
|
|
154
|
+
};
|
|
155
|
+
export type CorsOrigin = string | string[] | ((origin: string | undefined) => string | null | undefined);
|
|
156
|
+
export type CorsOptions = {
|
|
157
|
+
origin?: CorsOrigin;
|
|
158
|
+
methods?: string[];
|
|
159
|
+
allowedHeaders?: string[];
|
|
160
|
+
exposedHeaders?: string[];
|
|
161
|
+
credentials?: boolean;
|
|
162
|
+
maxAgeSeconds?: number;
|
|
163
|
+
};
|
|
164
|
+
export type CompressionOptions = {
|
|
165
|
+
threshold?: number;
|
|
166
|
+
level?: number;
|
|
167
|
+
};
|
|
168
|
+
export type QHTTPXPlugin<Options = any> = (app: any, // We use 'any' here to avoid circular dependency with QHTTPX class, or we could use an interface
|
|
169
|
+
options: Options) => void | Promise<void>;
|
|
170
|
+
export type QHTTPXPluginOptions = {
|
|
171
|
+
prefix?: string;
|
|
172
|
+
[key: string]: any;
|
|
173
|
+
};
|
|
@@ -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,25 @@
|
|
|
1
|
+
import { IncomingMessage } from 'http';
|
|
2
|
+
import { Duplex } from 'stream';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
export interface QHTTPXWebSocket extends WebSocket {
|
|
5
|
+
join(room: string): void;
|
|
6
|
+
leave(room: string): void;
|
|
7
|
+
id: string;
|
|
8
|
+
}
|
|
9
|
+
export type WSHandler = (ws: QHTTPXWebSocket, req: IncomingMessage) => void;
|
|
10
|
+
export declare class WebSocketManager {
|
|
11
|
+
private readonly requestIdGenerator;
|
|
12
|
+
private readonly wss;
|
|
13
|
+
private readonly handlers;
|
|
14
|
+
private readonly rooms;
|
|
15
|
+
constructor(requestIdGenerator: () => string);
|
|
16
|
+
register(path: string, handler: WSHandler): void;
|
|
17
|
+
handleUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer): Promise<void>;
|
|
18
|
+
private join;
|
|
19
|
+
private leave;
|
|
20
|
+
private leaveAll;
|
|
21
|
+
to(room: string): {
|
|
22
|
+
emit: (data: unknown) => void;
|
|
23
|
+
};
|
|
24
|
+
broadcast(data: unknown): void;
|
|
25
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebSocketManager = void 0;
|
|
4
|
+
const url_1 = require("url");
|
|
5
|
+
const ws_1 = require("ws");
|
|
6
|
+
class WebSocketManager {
|
|
7
|
+
constructor(requestIdGenerator) {
|
|
8
|
+
this.requestIdGenerator = requestIdGenerator;
|
|
9
|
+
this.handlers = [];
|
|
10
|
+
this.rooms = new Map();
|
|
11
|
+
this.wss = new ws_1.WebSocketServer({ noServer: true });
|
|
12
|
+
}
|
|
13
|
+
register(path, handler) {
|
|
14
|
+
this.handlers.push({ path, handler });
|
|
15
|
+
}
|
|
16
|
+
async handleUpgrade(req, socket, head) {
|
|
17
|
+
const rawUrl = req.url || '/';
|
|
18
|
+
const host = req.headers.host || 'localhost';
|
|
19
|
+
const urlObj = new url_1.URL(rawUrl, `http://${host}`);
|
|
20
|
+
const path = urlObj.pathname;
|
|
21
|
+
const handlerEntry = this.handlers.find((entry) => entry.path === path);
|
|
22
|
+
if (!handlerEntry) {
|
|
23
|
+
socket.destroy();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.wss.handleUpgrade(req, socket, head, (ws) => {
|
|
27
|
+
const qws = ws;
|
|
28
|
+
qws.id = this.requestIdGenerator();
|
|
29
|
+
qws.join = (room) => this.join(room, qws);
|
|
30
|
+
qws.leave = (room) => this.leave(room, qws);
|
|
31
|
+
qws.on('close', () => {
|
|
32
|
+
this.leaveAll(qws);
|
|
33
|
+
});
|
|
34
|
+
handlerEntry.handler(qws, req);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
join(room, ws) {
|
|
38
|
+
if (!this.rooms.has(room)) {
|
|
39
|
+
this.rooms.set(room, new Set());
|
|
40
|
+
}
|
|
41
|
+
this.rooms.get(room).add(ws);
|
|
42
|
+
}
|
|
43
|
+
leave(room, ws) {
|
|
44
|
+
const set = this.rooms.get(room);
|
|
45
|
+
if (set) {
|
|
46
|
+
set.delete(ws);
|
|
47
|
+
if (set.size === 0) {
|
|
48
|
+
this.rooms.delete(room);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
leaveAll(ws) {
|
|
53
|
+
for (const [room, set] of this.rooms) {
|
|
54
|
+
if (set.has(ws)) {
|
|
55
|
+
set.delete(ws);
|
|
56
|
+
if (set.size === 0) {
|
|
57
|
+
this.rooms.delete(room);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
to(room) {
|
|
63
|
+
return {
|
|
64
|
+
emit: (data) => {
|
|
65
|
+
const set = this.rooms.get(room);
|
|
66
|
+
if (set) {
|
|
67
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
68
|
+
for (const client of set) {
|
|
69
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
70
|
+
client.send(payload);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
broadcast(data) {
|
|
78
|
+
const payload = typeof data === 'string' ? data : JSON.stringify(data);
|
|
79
|
+
for (const client of this.wss.clients) {
|
|
80
|
+
if (client.readyState === ws_1.WebSocket.OPEN) {
|
|
81
|
+
client.send(payload);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
exports.WebSocketManager = WebSocketManager;
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
export type WorkItem<T> = {
|
|
6
|
+
id: number;
|
|
7
|
+
task: T;
|
|
8
|
+
priority: number;
|
|
9
|
+
};
|
|
10
|
+
export declare class WorkerQueue<T> {
|
|
11
|
+
private readonly capacity;
|
|
12
|
+
private readonly buffer;
|
|
13
|
+
private writeIndex;
|
|
14
|
+
private readIndex;
|
|
15
|
+
private size;
|
|
16
|
+
constructor(capacity?: number);
|
|
17
|
+
/**
|
|
18
|
+
* Enqueue a work item. Returns true if successful, false if queue is full.
|
|
19
|
+
*/
|
|
20
|
+
enqueue(item: WorkItem<T>): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Dequeue a work item. Returns undefined if queue is empty.
|
|
23
|
+
*/
|
|
24
|
+
dequeue(): WorkItem<T> | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Peek at the next item without removing it.
|
|
27
|
+
*/
|
|
28
|
+
peek(): WorkItem<T> | undefined;
|
|
29
|
+
/**
|
|
30
|
+
* Check if the queue is empty.
|
|
31
|
+
*/
|
|
32
|
+
isEmpty(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Get the current size of the queue.
|
|
35
|
+
*/
|
|
36
|
+
getSize(): number;
|
|
37
|
+
/**
|
|
38
|
+
* Get the capacity of the queue.
|
|
39
|
+
*/
|
|
40
|
+
getCapacity(): number;
|
|
41
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lock-free (or lock-minimal) work queue for per-worker task distribution.
|
|
4
|
+
* Uses a simple ring buffer for high throughput.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.WorkerQueue = void 0;
|
|
8
|
+
class WorkerQueue {
|
|
9
|
+
constructor(capacity = 1024) {
|
|
10
|
+
this.writeIndex = 0;
|
|
11
|
+
this.readIndex = 0;
|
|
12
|
+
this.size = 0;
|
|
13
|
+
if (capacity <= 0 || !Number.isInteger(capacity)) {
|
|
14
|
+
throw new Error('Capacity must be a positive integer');
|
|
15
|
+
}
|
|
16
|
+
// Ensure capacity is a power of 2 for efficient modulo with bitmask
|
|
17
|
+
this.capacity = Math.pow(2, Math.ceil(Math.log2(capacity)));
|
|
18
|
+
this.buffer = new Array(this.capacity);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Enqueue a work item. Returns true if successful, false if queue is full.
|
|
22
|
+
*/
|
|
23
|
+
enqueue(item) {
|
|
24
|
+
if (this.size >= this.capacity) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
this.buffer[this.writeIndex] = item;
|
|
28
|
+
this.writeIndex = (this.writeIndex + 1) & (this.capacity - 1);
|
|
29
|
+
this.size += 1;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Dequeue a work item. Returns undefined if queue is empty.
|
|
34
|
+
*/
|
|
35
|
+
dequeue() {
|
|
36
|
+
if (this.size <= 0) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const item = this.buffer[this.readIndex];
|
|
40
|
+
this.buffer[this.readIndex] = undefined;
|
|
41
|
+
this.readIndex = (this.readIndex + 1) & (this.capacity - 1);
|
|
42
|
+
this.size -= 1;
|
|
43
|
+
return item;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Peek at the next item without removing it.
|
|
47
|
+
*/
|
|
48
|
+
peek() {
|
|
49
|
+
if (this.size <= 0) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return this.buffer[this.readIndex];
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Check if the queue is empty.
|
|
56
|
+
*/
|
|
57
|
+
isEmpty() {
|
|
58
|
+
return this.size === 0;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Get the current size of the queue.
|
|
62
|
+
*/
|
|
63
|
+
getSize() {
|
|
64
|
+
return this.size;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Get the capacity of the queue.
|
|
68
|
+
*/
|
|
69
|
+
getCapacity() {
|
|
70
|
+
return this.capacity;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.WorkerQueue = WorkerQueue;
|
package/dist/src/cors.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCorsMiddleware = createCorsMiddleware;
|
|
4
|
+
function resolveOrigin(origin, requestOrigin) {
|
|
5
|
+
if (!origin) {
|
|
6
|
+
return '*';
|
|
7
|
+
}
|
|
8
|
+
if (typeof origin === 'string') {
|
|
9
|
+
return origin;
|
|
10
|
+
}
|
|
11
|
+
if (Array.isArray(origin)) {
|
|
12
|
+
if (!requestOrigin) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (origin.includes(requestOrigin)) {
|
|
16
|
+
return requestOrigin;
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return origin(requestOrigin);
|
|
21
|
+
}
|
|
22
|
+
function createCorsMiddleware(options = {}) {
|
|
23
|
+
const methodsHeader = options.methods && options.methods.length > 0
|
|
24
|
+
? options.methods.join(', ')
|
|
25
|
+
: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS';
|
|
26
|
+
const allowedHeadersHeader = options.allowedHeaders && options.allowedHeaders.length > 0
|
|
27
|
+
? options.allowedHeaders.join(', ')
|
|
28
|
+
: undefined;
|
|
29
|
+
const exposedHeadersHeader = options.exposedHeaders && options.exposedHeaders.length > 0
|
|
30
|
+
? options.exposedHeaders.join(', ')
|
|
31
|
+
: undefined;
|
|
32
|
+
const maxAgeHeader = typeof options.maxAgeSeconds === 'number'
|
|
33
|
+
? String(options.maxAgeSeconds)
|
|
34
|
+
: undefined;
|
|
35
|
+
const allowCredentials = options.credentials ?? false;
|
|
36
|
+
return async (ctx, next) => {
|
|
37
|
+
const requestOriginHeader = ctx.req.headers.origin;
|
|
38
|
+
const resolvedOrigin = resolveOrigin(options.origin, requestOriginHeader);
|
|
39
|
+
if (resolvedOrigin) {
|
|
40
|
+
ctx.res.setHeader('access-control-allow-origin', resolvedOrigin);
|
|
41
|
+
if (allowCredentials) {
|
|
42
|
+
ctx.res.setHeader('access-control-allow-credentials', 'true');
|
|
43
|
+
}
|
|
44
|
+
if (exposedHeadersHeader) {
|
|
45
|
+
ctx.res.setHeader('access-control-expose-headers', exposedHeadersHeader);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (ctx.req.method === 'OPTIONS') {
|
|
49
|
+
ctx.res.statusCode = 204;
|
|
50
|
+
ctx.res.setHeader('access-control-allow-methods', methodsHeader);
|
|
51
|
+
const requestHeaders = typeof ctx.req.headers['access-control-request-headers'] === 'string'
|
|
52
|
+
? ctx.req.headers['access-control-request-headers']
|
|
53
|
+
: undefined;
|
|
54
|
+
const headersValue = allowedHeadersHeader || requestHeaders;
|
|
55
|
+
if (headersValue) {
|
|
56
|
+
ctx.res.setHeader('access-control-allow-headers', headersValue);
|
|
57
|
+
}
|
|
58
|
+
if (maxAgeHeader) {
|
|
59
|
+
ctx.res.setHeader('access-control-max-age', maxAgeHeader);
|
|
60
|
+
}
|
|
61
|
+
ctx.res.end();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
await next();
|
|
65
|
+
};
|
|
66
|
+
}
|